feat: alignment on media import event shape
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- Old bDS: imports by source path plus optional metadata in project context.
|
||||||
- bDS2 now: imports with attrs including `source_path` and `project_id`.
|
- bDS2 now: imports with attrs including `source_path` and `project_id`.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ surface MediaControlSurface {
|
|||||||
facing _: MediaOperator
|
facing _: MediaOperator
|
||||||
|
|
||||||
provides:
|
provides:
|
||||||
ImportMediaRequested(project, source_file)
|
ImportMediaRequested(source_path, project, metadata)
|
||||||
UpdateMediaRequested(media, changes)
|
UpdateMediaRequested(media, changes)
|
||||||
DeleteMediaRequested(media)
|
DeleteMediaRequested(media)
|
||||||
UpsertMediaTranslationRequested(media, language, title, alt, caption)
|
UpsertMediaTranslationRequested(media, language, title, alt, caption)
|
||||||
@@ -121,22 +121,29 @@ invariant DateBasedMediaLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rule ImportMedia {
|
rule ImportMedia {
|
||||||
when: ImportMediaRequested(project, source_file)
|
when: ImportMediaRequested(source_path, project, metadata)
|
||||||
let uuid_name = generate_uuid() + extension(source_file)
|
-- 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}",
|
let dest = format("media/{yyyy}/{mm}/{uuid_name}",
|
||||||
yyyy: now.year, mm: now.month_padded)
|
yyyy: now.year, mm: now.month_padded)
|
||||||
ensures: Media.created(
|
ensures: Media.created(
|
||||||
project: project,
|
project: project,
|
||||||
filename: uuid_name,
|
filename: uuid_name,
|
||||||
original_name: source_file.name,
|
original_name: basename(source_path),
|
||||||
mime_type: detect_mime(source_file),
|
mime_type: detect_mime(source_path),
|
||||||
size: source_file.size,
|
size: file_size(source_path),
|
||||||
width: detect_width(source_file),
|
width: detect_width(source_path),
|
||||||
height: detect_height(source_file),
|
height: detect_height(source_path),
|
||||||
|
title: metadata.title,
|
||||||
|
alt: metadata.alt,
|
||||||
|
caption: metadata.caption,
|
||||||
|
author: metadata.author,
|
||||||
|
language: metadata.language,
|
||||||
file_path: dest,
|
file_path: dest,
|
||||||
tags: {}
|
tags: metadata.tags
|
||||||
)
|
)
|
||||||
ensures: FileCopied(source_file, dest)
|
ensures: FileCopied(source_path, dest)
|
||||||
ensures: SidecarWritten(media)
|
ensures: SidecarWritten(media)
|
||||||
ensures: ThumbnailsGenerated(media)
|
ensures: ThumbnailsGenerated(media)
|
||||||
ensures: SearchIndexUpdated(media)
|
ensures: SearchIndexUpdated(media)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ surface MediaProcessingControlSurface {
|
|||||||
facing _: MediaProcessingOperator
|
facing _: MediaProcessingOperator
|
||||||
|
|
||||||
provides:
|
provides:
|
||||||
ImportMediaRequested(source_path, project)
|
ImportMediaRequested(source_path, project, metadata)
|
||||||
TagMediaRequested(media, tags)
|
TagMediaRequested(media, tags)
|
||||||
DeleteMediaRequested(media)
|
DeleteMediaRequested(media)
|
||||||
ValidateMediaRequested(project)
|
ValidateMediaRequested(project)
|
||||||
@@ -239,11 +239,13 @@ invariant MediaTranslationFileLayout {
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|
||||||
rule ImportMedia {
|
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)
|
-- 1. Validate file type (must be supported image)
|
||||||
-- 2. Generate UUID v4 filename
|
-- 2. Generate UUID v4 filename
|
||||||
-- 3. Copy to media/{YYYY}/{MM}/{uuid}.{ext}
|
-- 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
|
-- 5. Generate four thumbnail sizes
|
||||||
-- 6. Index for search (FTS5)
|
-- 6. Index for search (FTS5)
|
||||||
ensures: media/Media.created(
|
ensures: media/Media.created(
|
||||||
@@ -253,6 +255,12 @@ rule ImportMedia {
|
|||||||
size: file_size(source_path),
|
size: file_size(source_path),
|
||||||
width: extract_width_from_header(source_path),
|
width: extract_width_from_header(source_path),
|
||||||
height: extract_height_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}"),
|
file_path: format("media/{yyyy}/{mm}/{uuid}.{ext}"),
|
||||||
sidecar_path: format("media/{yyyy}/{mm}/{uuid}.{ext}.meta"),
|
sidecar_path: format("media/{yyyy}/{mm}/{uuid}.{ext}.meta"),
|
||||||
checksum: sha256(source_path)
|
checksum: sha256(source_path)
|
||||||
|
|||||||
31
test/bds/alignment_test.exs
Normal file
31
test/bds/alignment_test.exs
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user