From 2c3b6f1a6b90b8901c3205f33d2ba037ab94175e Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 25 Jun 2025 21:02:20 +0200 Subject: [PATCH 01/28] wip --- .../InitialAttributeValuesResolver.php | 8 +- .../api/src/Attribute/AttributeAssigner.php | 2 +- .../InitialAttributeValuesResolverTest.php | 144 ++++++++++++++++++ 3 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php diff --git a/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php b/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php index ae2327084..270d0a308 100644 --- a/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php +++ b/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php @@ -12,7 +12,6 @@ use App\Entity\Core\AttributeDefinition; use App\File\FileMetadataAccessorWrapper; use App\Repository\Core\AttributeDefinitionRepository; -use Doctrine\ORM\EntityManagerInterface; use Twig\Environment; use Twig\Loader\ArrayLoader; @@ -21,7 +20,7 @@ class InitialAttributeValuesResolver private readonly Environment $twig; public function __construct( - private readonly EntityManagerInterface $em, + private readonly AttributeDefinitionRepository $attributeDefinitionRepository, private readonly AttributeAssigner $attributeAssigner, ) { $this->twig = new Environment(new ArrayLoader(), [ @@ -36,10 +35,7 @@ public function resolveInitialAttributes(Asset $asset): array { $attributes = []; - /** @var AttributeDefinitionRepository $repo */ - $repo = $this->em->getRepository(AttributeDefinition::class); - - $definitions = $repo->getWorkspaceInitializeDefinitions($asset->getWorkspaceId()); + $definitions = $this->attributeDefinitionRepository->getWorkspaceInitializeDefinitions($asset->getWorkspaceId()); $fileMetadataAccessorWrapper = new FileMetadataAccessorWrapper($asset->getSource()); foreach ($definitions as $definition) { diff --git a/databox/api/src/Attribute/AttributeAssigner.php b/databox/api/src/Attribute/AttributeAssigner.php index af2cd03e9..8135e9645 100644 --- a/databox/api/src/Attribute/AttributeAssigner.php +++ b/databox/api/src/Attribute/AttributeAssigner.php @@ -10,7 +10,7 @@ use App\Entity\Core\AbstractBaseAttribute; use App\Entity\Core\Attribute; -final readonly class AttributeAssigner +class AttributeAssigner { public function __construct(private AttributeTypeRegistry $attributeTypeRegistry) { diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php new file mode 100644 index 000000000..9d72881c8 --- /dev/null +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -0,0 +1,144 @@ + [ + 'definition' => [ + 'name' => 'Title', + 'isMultiple' => false, + 'initialValues' => [ + '_' => '{ "type": "metadata", "value": "XMP-dc:Title"}', + 'en' => '{ "type": "metadata", "value": "description"}', + ], + 'fieldType' => TextAttributeType::NAME, + ], + 'metadata' => [ + 'Composite:GPSPosition' => '48.8588443, 2.2943506', + 'XMP-dc:Title' => 'Test Title', + 'description' => 'Test Description', + ], + 'expected' => [ + 'Title' => [ + '_' => ['Test Title'], + 'en' => ['Test Description'], + ], + ], + ], + 'test2' => [ + 'definition' => [ + 'name' => 'Keywords', + 'isMultiple' => true, + 'initialValues' => [ + '_' => '{ "type": "metadata", "value": "IPTC:Keywords"}', + ], + 'fieldType' => TextAttributeType::NAME, + ], + 'metadata' => [ + 'IPTC:Keywords' => ['dog', 'cat', 'bird'], + ], + 'expected' => [ + 'Keywords' => [ + '_' => ['cat', 'dog', 'bird'], + ], + ], + ], + ]; + } + + /** + * @dataProvider dataProvider + * + * @param array $definition + * @param array $metadata + */ + public function testResolveInitialAttributes(array $definition, array $metadata, array $expected): void + { + self::bootKernel(); + $container = static::getContainer(); + + $atr = $this->createMock(AttributeDefinition::class); + $atr->expects($this->any())->method('getName') + ->willReturn($definition['name']); + $atr->expects($this->any())->method('isMultiple') + ->willReturn($definition['isMultiple']); + $atr->expects($this->any())->method('getInitialValues') + ->willReturn($definition['initialValues']); + $atr->expects($this->any())->method('getFieldType') + ->willReturn($definition['fieldType']); + + /** @var AttributeDefinitionRepository $adr */ + $adr = $this->createMock(AttributeDefinitionRepository::class); + $adr->expects($this->any()) + ->method('getWorkspaceInitializeDefinitions') + ->willReturn([$atr]); + + /** @var File $fileMock */ + $fileMock = $this->createMock(File::class); + + $data = is_array($metadata) ? $metadata : [$metadata]; + $fileMock->expects($this->any()) + ->method('getMetadata') + ->willReturn($this->toMetadata($metadata)); + + /** @var Asset $assetMock */ + $assetMock = $this->createMock(Asset::class); + $assetMock->expects($this->any()) + ->method('getSource') + ->willReturn($fileMock); + + // ================================================ + + $iavr = new InitialAttributeValuesResolver( + $adr, + $container->get(AttributeAssigner::class) + ); + + $result = []; + /** @var Attribute $attribute */ + foreach ($iavr->resolveInitialAttributes($assetMock) as $attribute) { + $result[$attribute->getDefinition()->getName()] ??= []; + $result[$attribute->getDefinition()->getName()][$attribute->getLocale()] ??= []; + $result[$attribute->getDefinition()->getName()][$attribute->getLocale()][] = $attribute->getValue(); + } + + $this->assertEqualsCanonicalizing($expected, $result); + } + + private function toMetadata($data) + { + $metadata = []; + foreach ($data as $key => $value) { + if (is_array($value)) { + $metadata[$key] = [ + 'value' => join(' ; ', $value), + 'values' => $value, + ]; + } else { + $metadata[$key] = [ + 'value' => $value, + 'values' => [$value], + ]; + } + } + + return $metadata; + } +} From af41bc5e4a60fffa1c52340a68ae97ec5b3f2988 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Fri, 27 Jun 2025 11:32:57 +0200 Subject: [PATCH 02/28] wip --- .../InitialAttributeValuesResolverTest.php | 166 ++++++++++-------- .../InitialAttributeValuesResolverData.yaml | 94 ++++++++++ 2 files changed, 187 insertions(+), 73 deletions(-) create mode 100644 databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 9d72881c8..018ad9fea 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -14,101 +14,80 @@ use App\Repository\Core\AttributeDefinitionRepository; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Yaml\Yaml; class InitialAttributeValuesResolverTest extends KernelTestCase { + private AttributeAssigner $attributeAssigner; + + public function setUp(): void + { + self::bootKernel(); + $this->attributeAssigner = static::getContainer()->get(AttributeAssigner::class); + } + public static function dataProvider(): array { - return [ - 'test1' => [ - 'definition' => [ - 'name' => 'Title', - 'isMultiple' => false, - 'initialValues' => [ - '_' => '{ "type": "metadata", "value": "XMP-dc:Title"}', - 'en' => '{ "type": "metadata", "value": "description"}', - ], - 'fieldType' => TextAttributeType::NAME, - ], - 'metadata' => [ - 'Composite:GPSPosition' => '48.8588443, 2.2943506', - 'XMP-dc:Title' => 'Test Title', - 'description' => 'Test Description', - ], - 'expected' => [ - 'Title' => [ - '_' => ['Test Title'], - 'en' => ['Test Description'], - ], - ], - ], - 'test2' => [ - 'definition' => [ - 'name' => 'Keywords', - 'isMultiple' => true, - 'initialValues' => [ - '_' => '{ "type": "metadata", "value": "IPTC:Keywords"}', - ], - 'fieldType' => TextAttributeType::NAME, - ], - 'metadata' => [ - 'IPTC:Keywords' => ['dog', 'cat', 'bird'], - ], - 'expected' => [ - 'Keywords' => [ - '_' => ['cat', 'dog', 'bird'], - ], - ], - ], - ]; + return array_map( + function ($test) { + return [ + $test['definitions'], + $test['metadata'], + $test['expected'], + ]; + }, + array_filter( + Yaml::parseFile(__DIR__.'/../../fixtures/metadata/InitialAttributeValuesResolverData.yaml'), + function ($test) { + return $test['enabled'] ?? true; + } + ) + ); } /** * @dataProvider dataProvider * - * @param array $definition - * @param array $metadata + * @param array $metadata */ - public function testResolveInitialAttributes(array $definition, array $metadata, array $expected): void + public function testResolveInitialAttributes(array $definitions, array $metadata, array $expected): void { - self::bootKernel(); - $container = static::getContainer(); - - $atr = $this->createMock(AttributeDefinition::class); - $atr->expects($this->any())->method('getName') - ->willReturn($definition['name']); - $atr->expects($this->any())->method('isMultiple') - ->willReturn($definition['isMultiple']); - $atr->expects($this->any())->method('getInitialValues') - ->willReturn($definition['initialValues']); - $atr->expects($this->any())->method('getFieldType') - ->willReturn($definition['fieldType']); - - /** @var AttributeDefinitionRepository $adr */ + $attributeDefinitions = []; + foreach ($definitions as $name => $definition) { + if (null !== ($initialValues = $definition['initialValues'] ?? null)) { + $initialValues = is_array($initialValues) ? $initialValues : ['_' => $initialValues]; + } + $ad = $this->createMock(AttributeDefinition::class); + $ad->expects($this->any())->method('getName') + ->willReturn($name); + $ad->expects($this->any())->method('isMultiple') + ->willReturn($definition['isMultiple'] ?? false); + $ad->expects($this->any())->method('getInitialValues') + ->willReturn($initialValues); + $ad->expects($this->any())->method('getFieldType') + ->willReturn($definition['fieldType'] ?? TextAttributeType::NAME); + $attributeDefinitions[] = $ad; + } + $adr = $this->createMock(AttributeDefinitionRepository::class); $adr->expects($this->any()) ->method('getWorkspaceInitializeDefinitions') - ->willReturn([$atr]); + ->willReturn($attributeDefinitions); - /** @var File $fileMock */ $fileMock = $this->createMock(File::class); - $data = is_array($metadata) ? $metadata : [$metadata]; $fileMock->expects($this->any()) ->method('getMetadata') - ->willReturn($this->toMetadata($metadata)); + ->willReturn($this->conformMetadata($metadata)); - /** @var Asset $assetMock */ $assetMock = $this->createMock(Asset::class); $assetMock->expects($this->any()) ->method('getSource') ->willReturn($fileMock); - // ================================================ - $iavr = new InitialAttributeValuesResolver( $adr, - $container->get(AttributeAssigner::class) + $this->attributeAssigner ); $result = []; @@ -119,26 +98,67 @@ public function testResolveInitialAttributes(array $definition, array $metadata, $result[$attribute->getDefinition()->getName()][$attribute->getLocale()][] = $attribute->getValue(); } - $this->assertEqualsCanonicalizing($expected, $result); + $this->assertEquals($this->conformExpected($expected), $result); + } + + private function conformExpected(array $expected): array + { + $conformed = []; + foreach ($expected as $attributeName => $value) { + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple list of values + $conformed[$attributeName] = ['_' => $value]; + } else { + // an array with key=locale + $conformed[$attributeName] = array_map( + function ($v) { + return is_array($v) ? $v : [$v]; + }, + $value + ); + } + } else { + // a single value + $conformed[$attributeName] = ['_' => [$value]]; + } + } + + return $conformed; + } + + private function isNumericArray($a): bool + { + if (!is_array($a)) { + return false; + } + foreach ($a as $k => $v) { + if (!is_numeric($k)) { + return false; + } + } + + return true; } - private function toMetadata($data) + private function conformMetadata($data): array { - $metadata = []; + $conformed = []; + $data = is_array($data) ? $data : [$data]; foreach ($data as $key => $value) { if (is_array($value)) { - $metadata[$key] = [ + $conformed[$key] = [ 'value' => join(' ; ', $value), 'values' => $value, ]; } else { - $metadata[$key] = [ + $conformed[$key] = [ 'value' => $value, 'values' => [$value], ]; } } - return $metadata; + return $conformed; } } diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml new file mode 100644 index 000000000..d0e94698e --- /dev/null +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -0,0 +1,94 @@ +# shortcuts +# +# - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. +# if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) +# - same principle applies for expected/ values (no locale == '_' locale) +# +# - expected values is an array, possibly with only one element +# if only one value is expected, the plain value can be used (it will test with [value]) +# +# - fieldType defaults to text +# +# - isMultiple defaults to false +# +# - possible fieldTypes are: +# boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text + +exiftoolVersion: + about: + title: 'Vérification du fonctionnement de l''intégration metadata-read' + description: + 'Cette lecture permet de s''assurer du bon déroulement du processus d''extraction des métadonnées. + La metadata _ExifTool:ExifToolVersion_ est toujours retournée par exiftool.' + definitions: + exiftoolVersion: + initialValues: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' + metadata: + 'ExifTool:ExifToolVersion': '12.42' + expected: + exiftoolVersion: '12.42' + +multi2multi: + about: + title: 'Meta multi-valuée -> attribut multi-valué (méthode "metadata")' + definitions: + keywords: + isMultiple: true + initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' + metadata: + 'IPTC:Keywords': ['keyword1', 'keyword2', 'keyword3'] + expected: + keywords: ['keyword1', 'keyword2', 'keyword3'] + +exiftoolVersion2: + enabled: false + definitions: + exiftoolVersion: + fieldType: text + isMultiple: false + initialValues: + _: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' + metadata: + 'ExifTool:ExifToolVersion': '12.42' + expected: + exiftoolVersion: + _: + - '12.42' +test1: + enabled: false + definitions: + Title: + isMultiple: false + initialValues: + _: '{ "type": "metadata", "value": "XMP-dc:Title"}' + en: '{ "type": "metadata", "value": "description"}' + fieldType: text + metadata: + 'Composite:GPSPosition': '48.8588443, 2.2943506' + 'XMP-dc:Title': Test Title + description: Test Description + expected: + Title: + _: + - Test Title + en: + - Test Description +test2: + enabled: false + definitions: + Keywords: + isMultiple: true + initialValues: + _: '{ "type": "metadata", "value": "IPTC:Keywords"}' + fieldType: text + metadata: + 'IPTC:Keywords': + - dog + - cat + - bird + expected: + Keywords: + _: + - cat + - dog + - bird From 1bbba711b2c5d6858dce90bf5da90de7e3f2ad2f Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 2 Jul 2025 11:22:00 +0200 Subject: [PATCH 03/28] wip --- .../Command/DocumentationDumperCommand.php | 114 +++++++++++++++++- .../InitialAttributeValuesResolverData.yaml | 104 +++++++++------- 2 files changed, 174 insertions(+), 44 deletions(-) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index f927639d6..3cd3c7b02 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Yaml\Yaml; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command @@ -21,9 +22,118 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); - $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + // $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); + // $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + + $this->dumpInitialValuesDocumentation($output); return Command::SUCCESS; } + + private function dumpInitialValuesDocumentation(OutputInterface $output) + { + $n = 0; + foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { + if (!($test['about'] ?? false)) { + continue; + } + + if ($n++ > 0) { + $output->writeln(''); + $output->writeln('---'); + $output->writeln(''); + } + + $output->writeln(sprintf('## %s', $test['about']['title'] ?? '')); + if ($description = $test['about']['description'] ?? '') { + $output->writeln(sprintf('%s', $description)); + } + + $output->writeln('### Attribute(s) definition(s) ...'); + foreach ($test['definitions'] as $name => $definition) { + $output->write(sprintf('- __%s__:', $name)); + $output->write(sprintf(' `%s`', $definition['fieldType'] ?? 'text')); + $output->write($definition['isMultiple'] ?? false ? sprintf(' ; [x] `multi`') : ''); + + $output->writeln(''); + + if (is_array($definition['initialValues'] ?? null)) { + foreach ($definition['initialValues'] as $locale => $initializer) { + $output->writeln(sprintf(' - locale `%s`', $locale)); + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 1); + } + } elseif (null !== ($definition['initialValues'] ?? null)) { + $initializer = $definition['initialValues']; + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 0); + } + + $nCols = 0; + foreach ($test['metadata'] as $values) { + $nCols = max($nCols, is_array($values) ? count($values) : 1); + } + foreach ($test['metadata'] as $metadataName => $values) { + $v = is_array($values) ? $values : [$values]; + $output->writeln(sprintf('| %s | %s |', $metadataName, join(' | ', $v))); + } + + $output->writeln('### ... initial attribute(s) value(s)'); + foreach ($test['expected'] as $attributeName => $value) { + // $output->writeln(sprintf('- __%s__:', $attributeName)); + $this->dumpExpected($output, $attributeName, $value, 1); + } + } + + } + } + + private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 1): void + { + $tab = str_repeat(' ', $indent); + $output->writeln(sprintf('%s```%s', $tab, $language)); + foreach (explode("\n", $code) as $line) { + $output->writeln(sprintf('%s%s', $tab, $line)); + } + $output->writeln(sprintf('%s```', $tab)); + } + + private function dumpExpected(OutputInterface $output, string $attributeName, $value, int $indent = 0): void + { + $tab = str_repeat(' ', $indent); + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple list of values + $n = 1; + foreach ($value as $v) { + $output->writeln(sprintf('%s%s #%d: `%s`', $tab, $attributeName, $n++, $v)); + $output->writeln(''); + } + } else { + // an array with key=locale + foreach ($value as $locale => $v) { + $output->writeln(sprintf('%s- locale `%s`:', $tab, $locale)); + $output->writeln(''); + $this->dumpExpected($output, $attributeName, $v, $indent + 1); + } + } + } else { + // a single value + $output->writeln(sprintf('%s`%s: %s`', $tab, $attributeName, $value)); + } + } + + private function isNumericArray($a): bool + { + if (!is_array($a)) { + return false; + } + foreach ($a as $k => $v) { + if (!is_numeric($k)) { + return false; + } + } + + return true; + } } diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index d0e94698e..7c27db4c8 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -19,7 +19,7 @@ exiftoolVersion: title: 'Vérification du fonctionnement de l''intégration metadata-read' description: 'Cette lecture permet de s''assurer du bon déroulement du processus d''extraction des métadonnées. - La metadata _ExifTool:ExifToolVersion_ est toujours retournée par exiftool.' + La metadata `ExifTool:ExifToolVersion` est toujours retournée par exiftool.' definitions: exiftoolVersion: initialValues: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' @@ -28,67 +28,87 @@ exiftoolVersion: expected: exiftoolVersion: '12.42' +multi2mono: + about: + title: 'Meta multi-valuée -> attribut mono-valué (méthode "metadata")' + description: + 'Les valeurs seront séparées par des points-virgules.' + definitions: + Keywords: + isMultiple: false + initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' + metadata: + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + expected: + Keywords: 'chien ; chat ; oiseau' + multi2multi: about: title: 'Meta multi-valuée -> attribut multi-valué (méthode "metadata")' definitions: - keywords: + Keywords: isMultiple: true initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' metadata: - 'IPTC:Keywords': ['keyword1', 'keyword2', 'keyword3'] + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: - keywords: ['keyword1', 'keyword2', 'keyword3'] + Keywords: ['chien', 'chat', 'oiseau'] -exiftoolVersion2: - enabled: false +multi2mono_template: + about: + title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' + description: 'attention : utilisation erronée de getMetadata(...).value (sans "s")' definitions: - exiftoolVersion: - fieldType: text - isMultiple: false - initialValues: - _: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' + Keywords: + isMultiple: true + initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' metadata: - 'ExifTool:ExifToolVersion': '12.42' + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: - exiftoolVersion: - _: - - '12.42' -test1: - enabled: false + Keywords: 'chien ; chat ; oiseau' + +multi2multi_template: + about: + title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' + description: 'Utilisation correcte de getMetadata(...).value__s__' definitions: - Title: - isMultiple: false + Keywords: + isMultiple: true + initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values }}"}' + metadata: + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + expected: + Keywords: ['chien', 'chat', 'oiseau'] + +mono_multilingue: + about: + title: 'mono multilingue' + definitions: + Copyright: initialValues: - _: '{ "type": "metadata", "value": "XMP-dc:Title"}' - en: '{ "type": "metadata", "value": "description"}' - fieldType: text + en: '{ "type": "template", "value": "(c) {{ file.getMetadata(''XMP-dc:Creator'').value }}. All rights reserved" }' + fr: '{ "type": "template", "value": "(c) {{ file.getMetadata(''XMP-dc:Creator'').value }}. Tous droits réservés" }' metadata: - 'Composite:GPSPosition': '48.8588443, 2.2943506' - 'XMP-dc:Title': Test Title - description: Test Description + 'XMP-dc:Creator': 'Bob' expected: - Title: - _: - - Test Title - en: - - Test Description -test2: - enabled: false + Copyright: + en: '(c) Bob. All rights reserved' + fr: '(c) Bob. Tous droits réservés' + +multi_multilingue: + about: + title: 'multi multilingue' definitions: Keywords: isMultiple: true initialValues: - _: '{ "type": "metadata", "value": "IPTC:Keywords"}' - fieldType: text + en: '{ "type": "metadata", "value": "XMP-dc:Subject" }' + fr: '{ "type": "metadata", "value": "IPTC:SupplementalCategories" }' metadata: - 'IPTC:Keywords': - - dog - - cat - - bird + 'XMP-dc:Subject': ['dog', 'cat', 'bird'] + 'IPTC:SupplementalCategories': ['chien', 'chat', 'oiseau'] expected: Keywords: - _: - - cat - - dog - - bird + en: ['dog', 'cat', 'bird'] + fr: ['chien', 'chat', 'oiseau'] + From d195a253f720e3f165e4fe37c5c5c341226035ae Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 2 Jul 2025 20:33:51 +0200 Subject: [PATCH 04/28] wip --- .../Command/DocumentationDumperCommand.php | 105 ++++++++++++------ .../InitialAttributeValuesResolverTest.php | 2 + 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 3cd3c7b02..4eee82ca8 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -7,6 +7,7 @@ use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Yaml; @@ -20,18 +21,46 @@ public function __construct( parent::__construct(); } + protected function configure(): void + { + parent::configure(); + + $this + ->setDescription('Dump code-generated documentation(s)') + ->addArgument('part', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Part(s) to dump. If not specified, all parts will be dumped.') + ->setHelp('parts: "rendition", "initial-attribute"') + ; + } + protected function execute(InputInterface $input, OutputInterface $output): int { - // $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); - // $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + if (empty($input->getArgument('part'))) { + $input->setArgument('part', ['rendition', 'initial-attribute']); + } - $this->dumpInitialValuesDocumentation($output); + foreach ($input->getArgument('part') as $part) { + if (!in_array($part, ['rendition', 'initial-attribute'])) { + $output->writeln(sprintf('Unknown part "%s". Valid parts are "rendition" and "initial-attribute".', $part)); + + return Command::FAILURE; + } + switch ($part) { + case 'rendition': + $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); + $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + break; + case 'initial-attribute': + $this->dumpInitialValuesDocumentation($output); + break; + } + } return Command::SUCCESS; } private function dumpInitialValuesDocumentation(OutputInterface $output) { + $output->writeln('# Initial Attribute Values'); $n = 0; foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { if (!($test['about'] ?? false)) { @@ -53,7 +82,8 @@ private function dumpInitialValuesDocumentation(OutputInterface $output) foreach ($test['definitions'] as $name => $definition) { $output->write(sprintf('- __%s__:', $name)); $output->write(sprintf(' `%s`', $definition['fieldType'] ?? 'text')); - $output->write($definition['isMultiple'] ?? false ? sprintf(' ; [x] `multi`') : ''); + $output->write(sprintf(' ; [%s] `multi`', $definition['isMultiple'] ?? false ? 'X' : ' ')); + $output->write(sprintf(' ; [%s] `translatable`', $definition['isTranslatable'] ?? false ? 'X' : ' ')); $output->writeln(''); @@ -69,26 +99,26 @@ private function dumpInitialValuesDocumentation(OutputInterface $output) $this->codeBlockIndented($output, $code, 'json', 0); } - $nCols = 0; - foreach ($test['metadata'] as $values) { - $nCols = max($nCols, is_array($values) ? count($values) : 1); - } + $output->writeln(''); + $output->writeln('### ... with file metadata ...'); + $output->writeln('| metadata | value(s) |'); + $output->writeln('|---|---|'); foreach ($test['metadata'] as $metadataName => $values) { $v = is_array($values) ? $values : [$values]; - $output->writeln(sprintf('| %s | %s |', $metadataName, join(' | ', $v))); + $output->writeln(sprintf('| %s | `%s` |', $metadataName, join('` ; `', $v))); } - $output->writeln('### ... initial attribute(s) value(s)'); - foreach ($test['expected'] as $attributeName => $value) { - // $output->writeln(sprintf('- __%s__:', $attributeName)); - $this->dumpExpected($output, $attributeName, $value, 1); - } + $output->writeln(''); + $output->writeln('### ... set attribute(s) initial value(s)'); + $this->dumpExpected($output, $test['expected'], 1); + + $output->writeln(''); } } } - private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 1): void + private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 0): void { $tab = str_repeat(' ', $indent); $output->writeln(sprintf('%s```%s', $tab, $language)); @@ -98,28 +128,39 @@ private function codeBlockIndented(OutputInterface $output, string $code, string $output->writeln(sprintf('%s```', $tab)); } - private function dumpExpected(OutputInterface $output, string $attributeName, $value, int $indent = 0): void + private function dumpExpected(OutputInterface $output, array $expected, int $indent = 0): void { $tab = str_repeat(' ', $indent); - if (is_array($value)) { - if ($this->isNumericArray($value)) { - // a simple list of values - $n = 1; - foreach ($value as $v) { - $output->writeln(sprintf('%s%s #%d: `%s`', $tab, $attributeName, $n++, $v)); - $output->writeln(''); + $output->writeln(sprintf('%s| Attributes | initial value(s) |', $tab)); + $output->writeln(sprintf('%s|---|---|', $tab)); + foreach ($expected as $attributeName => $value) { + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple array of values + $n = 1; + foreach ($value as $v) { + $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n++, $v)); + // $output->writeln(''); + } + } else { + // an array with key=locale + foreach ($value as $locale => $v) { + $output->writeln(sprintf('%s| __locale `%s`__ | |', $tab, $locale)); + if (is_array($v)) { + // an array of values + foreach ($v as $n => $w) { + $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n + 1, $w)); + } + } else { + $output->writeln(sprintf('%s| %s | `%s` |', $tab, $attributeName, $v)); + } + // $output->writeln(''); + } } } else { - // an array with key=locale - foreach ($value as $locale => $v) { - $output->writeln(sprintf('%s- locale `%s`:', $tab, $locale)); - $output->writeln(''); - $this->dumpExpected($output, $attributeName, $v, $indent + 1); - } + // a single value + $output->writeln(sprintf('%s| %s | `%s`', $tab, $attributeName, $value)); } - } else { - // a single value - $output->writeln(sprintf('%s`%s: %s`', $tab, $attributeName, $value)); } } diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 018ad9fea..c78b115fd 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -62,6 +62,8 @@ public function testResolveInitialAttributes(array $definitions, array $metadata ->willReturn($name); $ad->expects($this->any())->method('isMultiple') ->willReturn($definition['isMultiple'] ?? false); + $ad->expects($this->any())->method('isTranslatable') + ->willReturn($definition['isTranslatable'] ?? false); $ad->expects($this->any())->method('getInitialValues') ->willReturn($initialValues); $ad->expects($this->any())->method('getFieldType') From cb1ce743aa2960ccd8f56c0e0149988599bc95dd Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 2 Jul 2025 20:47:46 +0200 Subject: [PATCH 05/28] fix --- .../fixtures/metadata/InitialAttributeValuesResolverData.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index 7c27db4c8..43230d2c3 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -61,7 +61,7 @@ multi2mono_template: definitions: Keywords: isMultiple: true - initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' + initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' metadata: 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: @@ -74,7 +74,7 @@ multi2multi_template: definitions: Keywords: isMultiple: true - initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values }}"}' + initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values | join(''\n'') }}"}' metadata: 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: From 2c17f368972c76b5af993fdf2b9ac7e607e2c055 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 3 Jul 2025 18:13:04 +0200 Subject: [PATCH 06/28] auto-find documentation generators ; output to stdout or file --- .../Command/DocumentationDumperCommand.php | 178 +++++------------- .../DocumentationGeneratorInterface.php | 17 ++ .../InitialValuesDocumentationGenerator.php | 136 +++++++++++++ ...RenditionBuilderDocumentationGenerator.php | 22 +++ 4 files changed, 218 insertions(+), 135 deletions(-) create mode 100644 databox/api/src/documentation/DocumentationGeneratorInterface.php create mode 100644 databox/api/src/documentation/InitialValuesDocumentationGenerator.php create mode 100644 databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 4eee82ca8..8c86ddaee 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -4,19 +4,28 @@ namespace App\Command; -use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; +use App\documentation\DocumentationGeneratorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Yaml\Yaml; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; +use Symfony\Component\String\Slugger\AsciiSlugger; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command { + /** + * @uses InitialValuesDocumentationGenerator + * @uses RenditionBuilderDocumentationGenerator + */ + + /** @var array */ + private array $chapters = []; + public function __construct( - private readonly RenditionBuilderConfigurationDocumentation $renditionBuilderConfigurationDocumentation, + #[AutowireIterator(DocumentationGeneratorInterface::TAG)] private readonly iterable $documentations, ) { parent::__construct(); } @@ -25,156 +34,55 @@ protected function configure(): void { parent::configure(); + $slugger = new AsciiSlugger(); + /** @var DocumentationGeneratorInterface $documentation */ + foreach ($this->documentations as $documentation) { + $name = strtolower($slugger->slug($documentation::getName())->toString()); + if (isset($this->chapters[$documentation::getName()])) { + throw new \LogicException(sprintf('Chapter "%s" is already registered.', $name)); + } + $this->chapters[$name] = $documentation; + } + $this ->setDescription('Dump code-generated documentation(s)') - ->addArgument('part', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Part(s) to dump. If not specified, all parts will be dumped.') - ->setHelp('parts: "rendition", "initial-attribute"') + ->addArgument('chapters', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Chapter(s) to dump. If not specified, all chapters will be dumped.') + ->addOption('output', 'o', InputArgument::OPTIONAL, 'Output directory to write the documentation to. If not specified, it will be written to stdout.') + ->setHelp(sprintf('chapters: "%s"', join('", "', array_keys($this->chapters)))) ; } protected function execute(InputInterface $input, OutputInterface $output): int { - if (empty($input->getArgument('part'))) { - $input->setArgument('part', ['rendition', 'initial-attribute']); - } - - foreach ($input->getArgument('part') as $part) { - if (!in_array($part, ['rendition', 'initial-attribute'])) { - $output->writeln(sprintf('Unknown part "%s". Valid parts are "rendition" and "initial-attribute".', $part)); + $outputDir = $input->getOption('output'); + if ($outputDir && !is_dir($outputDir)) { + $output->writeln(sprintf('Output directory "%s" does not exists.', $outputDir)); - return Command::FAILURE; - } - switch ($part) { - case 'rendition': - $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); - $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); - break; - case 'initial-attribute': - $this->dumpInitialValuesDocumentation($output); - break; - } + return Command::FAILURE; } - return Command::SUCCESS; - } - - private function dumpInitialValuesDocumentation(OutputInterface $output) - { - $output->writeln('# Initial Attribute Values'); - $n = 0; - foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { - if (!($test['about'] ?? false)) { - continue; - } - - if ($n++ > 0) { - $output->writeln(''); - $output->writeln('---'); - $output->writeln(''); - } - - $output->writeln(sprintf('## %s', $test['about']['title'] ?? '')); - if ($description = $test['about']['description'] ?? '') { - $output->writeln(sprintf('%s', $description)); - } - - $output->writeln('### Attribute(s) definition(s) ...'); - foreach ($test['definitions'] as $name => $definition) { - $output->write(sprintf('- __%s__:', $name)); - $output->write(sprintf(' `%s`', $definition['fieldType'] ?? 'text')); - $output->write(sprintf(' ; [%s] `multi`', $definition['isMultiple'] ?? false ? 'X' : ' ')); - $output->write(sprintf(' ; [%s] `translatable`', $definition['isTranslatable'] ?? false ? 'X' : ' ')); - - $output->writeln(''); - - if (is_array($definition['initialValues'] ?? null)) { - foreach ($definition['initialValues'] as $locale => $initializer) { - $output->writeln(sprintf(' - locale `%s`', $locale)); - $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - $this->codeBlockIndented($output, $code, 'json', 1); - } - } elseif (null !== ($definition['initialValues'] ?? null)) { - $initializer = $definition['initialValues']; - $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - $this->codeBlockIndented($output, $code, 'json', 0); - } - - $output->writeln(''); - $output->writeln('### ... with file metadata ...'); - $output->writeln('| metadata | value(s) |'); - $output->writeln('|---|---|'); - foreach ($test['metadata'] as $metadataName => $values) { - $v = is_array($values) ? $values : [$values]; - $output->writeln(sprintf('| %s | `%s` |', $metadataName, join('` ; `', $v))); - } - - $output->writeln(''); - $output->writeln('### ... set attribute(s) initial value(s)'); - $this->dumpExpected($output, $test['expected'], 1); + foreach ($input->getArgument('chapters') as $chapter) { + if (!isset($this->chapters[$chapter])) { + $output->writeln(sprintf('Unknown chapter "%s". Available chapters are "%s"', $chapter, join('", "', array_keys($this->chapters)))); - $output->writeln(''); + return Command::FAILURE; } - } - } - private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 0): void - { - $tab = str_repeat(' ', $indent); - $output->writeln(sprintf('%s```%s', $tab, $language)); - foreach (explode("\n", $code) as $line) { - $output->writeln(sprintf('%s%s', $tab, $line)); + if (empty($input->getArgument('chapters'))) { + $input->setArgument('chapters', array_keys($this->chapters)); } - $output->writeln(sprintf('%s```', $tab)); - } - - private function dumpExpected(OutputInterface $output, array $expected, int $indent = 0): void - { - $tab = str_repeat(' ', $indent); - $output->writeln(sprintf('%s| Attributes | initial value(s) |', $tab)); - $output->writeln(sprintf('%s|---|---|', $tab)); - foreach ($expected as $attributeName => $value) { - if (is_array($value)) { - if ($this->isNumericArray($value)) { - // a simple array of values - $n = 1; - foreach ($value as $v) { - $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n++, $v)); - // $output->writeln(''); - } - } else { - // an array with key=locale - foreach ($value as $locale => $v) { - $output->writeln(sprintf('%s| __locale `%s`__ | |', $tab, $locale)); - if (is_array($v)) { - // an array of values - foreach ($v as $n => $w) { - $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n + 1, $w)); - } - } else { - $output->writeln(sprintf('%s| %s | `%s` |', $tab, $attributeName, $v)); - } - // $output->writeln(''); - } - } + foreach ($input->getArgument('chapters') as $chapter) { + $text = '# '.$this->chapters[$chapter]->getName()."\n".$this->chapters[$chapter]->generate(); + if ($outputDir) { + $outputFile = rtrim($outputDir, '/').'/'.$chapter.'.md'; + file_put_contents($outputFile, $text); + $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); } else { - // a single value - $output->writeln(sprintf('%s| %s | `%s`', $tab, $attributeName, $value)); + $output->writeln($text); } } - } - private function isNumericArray($a): bool - { - if (!is_array($a)) { - return false; - } - foreach ($a as $k => $v) { - if (!is_numeric($k)) { - return false; - } - } - - return true; + return Command::SUCCESS; } } diff --git a/databox/api/src/documentation/DocumentationGeneratorInterface.php b/databox/api/src/documentation/DocumentationGeneratorInterface.php new file mode 100644 index 000000000..8e2126c4b --- /dev/null +++ b/databox/api/src/documentation/DocumentationGeneratorInterface.php @@ -0,0 +1,17 @@ + 0) { + $output .= "\n---\n"; + } + + $output .= sprintf("## %s\n", $test['about']['title'] ?? ''); + if ($description = $test['about']['description'] ?? '') { + $output .= sprintf("%s\n", $description); + } + + $output .= "### Attribute(s) definition(s) ...\n"; + foreach ($test['definitions'] as $name => $definition) { + $output .= sprintf("- __%s__: `%s` ; [%s] `multi` ; [%s] `translatable`\n", + $name, + $definition['fieldType'] ?? 'text', + $definition['isMultiple'] ?? false ? 'X' : ' ', + $definition['isTranslatable'] ?? false ? 'X' : ' ' + ); + + $output .= "\n"; + + if (is_array($definition['initialValues'] ?? null)) { + foreach ($definition['initialValues'] as $locale => $initializer) { + $output .= sprintf(" - locale `%s`\n", $locale); + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 1); + } + } elseif (null !== ($definition['initialValues'] ?? null)) { + $initializer = $definition['initialValues']; + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 0); + } + + $output .= "\n"; + + $output .= "### ... with file metadata ...\n"; + $output .= "| metadata | value(s) |\n"; + $output .= "|---|---|\n"; + foreach ($test['metadata'] as $metadataName => $values) { + $v = is_array($values) ? $values : [$values]; + $output .= sprintf("| %s | `%s` |\n", $metadataName, join('` ; `', $v)); + } + + $output .= "\n"; + + $output .= "### ... set attribute(s) initial value(s)\n"; + $this->dumpExpected($output, $test['expected'], 1); + + $output .= "\n"; + } + } + + return $output; + } + + private function codeBlockIndented(string &$output, string $code, string $language, int $indent = 0): void + { + $tab = str_repeat(' ', $indent); + $output .= sprintf("%s```%s\n", $tab, $language); + foreach (explode("\n", $code) as $line) { + $output .= sprintf("%s%s\n", $tab, $line); + } + $output .= sprintf("%s```\n", $tab); + } + + private function dumpExpected(string &$output, array $expected, int $indent = 0): void + { + $tab = str_repeat(' ', $indent); + $output .= sprintf("%s| Attributes | initial value(s) |\n", $tab); + $output .= sprintf("%s|---|---|\n", $tab); + foreach ($expected as $attributeName => $value) { + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple array of values + $n = 1; + foreach ($value as $v) { + $output .= sprintf("%s| %s #%d | `%s` |\n", $tab, $attributeName, $n++, $v); + } + } else { + // an array with key=locale + foreach ($value as $locale => $v) { + $output .= sprintf("%s| __locale `%s`__ | |\n", $tab, $locale); + if (is_array($v)) { + // an array of values + foreach ($v as $n => $w) { + $output .= sprintf("%s| %s #%d | `%s` |\n", $tab, $attributeName, $n + 1, $w); + } + } else { + $output .= sprintf("%s| %s | `%s` |\n", $tab, $attributeName, $v); + } + } + } + } else { + // a single value + $output .= sprintf("%s| %s | `%s`\n", $tab, $attributeName, $value); + } + } + } + + private function isNumericArray($a): bool + { + if (!is_array($a)) { + return false; + } + foreach ($a as $k => $v) { + if (!is_numeric($k)) { + return false; + } + } + + return true; + } +} diff --git a/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php new file mode 100644 index 000000000..17c83aebc --- /dev/null +++ b/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php @@ -0,0 +1,22 @@ +renditionBuilderConfigurationDocumentation->generate(); + } +} From eb88c0ff48dffe5626151fdc1c3b7fcea2a8aec0 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 3 Jul 2025 18:32:46 +0200 Subject: [PATCH 07/28] cs --- databox/api/src/Command/DocumentationDumperCommand.php | 2 +- .../DocumentationGeneratorInterface.php | 4 ++-- .../InitialValuesDocumentationGenerator.php | 2 +- .../RenditionBuilderDocumentationGenerator.php | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) rename databox/api/src/{documentation => Documentation}/DocumentationGeneratorInterface.php (74%) rename databox/api/src/{documentation => Documentation}/InitialValuesDocumentationGenerator.php (99%) rename databox/api/src/{documentation => Documentation}/RenditionBuilderDocumentationGenerator.php (90%) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 8c86ddaee..a27e7d9c1 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -4,7 +4,7 @@ namespace App\Command; -use App\documentation\DocumentationGeneratorInterface; +use App\Documentation\DocumentationGeneratorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; diff --git a/databox/api/src/documentation/DocumentationGeneratorInterface.php b/databox/api/src/Documentation/DocumentationGeneratorInterface.php similarity index 74% rename from databox/api/src/documentation/DocumentationGeneratorInterface.php rename to databox/api/src/Documentation/DocumentationGeneratorInterface.php index 8e2126c4b..06b0f1b2d 100644 --- a/databox/api/src/documentation/DocumentationGeneratorInterface.php +++ b/databox/api/src/Documentation/DocumentationGeneratorInterface.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace App\documentation; +namespace App\Documentation; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; #[AutoconfigureTag(self::TAG)] interface DocumentationGeneratorInterface { - final public const TAG = 'documentation_generator'; + final public const string TAG = 'documentation_generator'; public static function getName(): string; diff --git a/databox/api/src/documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php similarity index 99% rename from databox/api/src/documentation/InitialValuesDocumentationGenerator.php rename to databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 3cfa4b076..0d6be4e7e 100644 --- a/databox/api/src/documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\documentation; +namespace App\Documentation; use Symfony\Component\Yaml\Yaml; diff --git a/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php similarity index 90% rename from databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php rename to databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index 17c83aebc..f985625cd 100644 --- a/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -1,6 +1,8 @@ Date: Mon, 7 Jul 2025 12:08:36 +0200 Subject: [PATCH 08/28] add multi-chapters (recursive), with numbered levels change test "about" to 'en' --- .../Command/DocumentationDumperCommand.php | 42 ++++++++++++---- .../Documentation/DocumentationGenerator.php | 41 +++++++++++++++ .../DocumentationGeneratorInterface.php | 7 +-- .../InitialValuesDocumentationGenerator.php | 19 +++++-- ...RenditionBuilderDocumentationGenerator.php | 11 ++-- .../RootDocumentationGenerator.php | 35 +++++++++++++ .../InitialAttributeValuesResolverData.yaml | 50 ++++++++++--------- 7 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 databox/api/src/Documentation/DocumentationGenerator.php create mode 100644 databox/api/src/Documentation/RootDocumentationGenerator.php diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index a27e7d9c1..88721f8e7 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -11,16 +11,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; -use Symfony\Component\String\Slugger\AsciiSlugger; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command { - /** - * @uses InitialValuesDocumentationGenerator - * @uses RenditionBuilderDocumentationGenerator - */ - /** @var array */ private array $chapters = []; @@ -34,11 +28,10 @@ protected function configure(): void { parent::configure(); - $slugger = new AsciiSlugger(); /** @var DocumentationGeneratorInterface $documentation */ foreach ($this->documentations as $documentation) { - $name = strtolower($slugger->slug($documentation::getName())->toString()); - if (isset($this->chapters[$documentation::getName()])) { + $name = $documentation->getName(); + if (isset($this->chapters[$name])) { throw new \LogicException(sprintf('Chapter "%s" is already registered.', $name)); } $this->chapters[$name] = $documentation; @@ -73,7 +66,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input->setArgument('chapters', array_keys($this->chapters)); } foreach ($input->getArgument('chapters') as $chapter) { - $text = '# '.$this->chapters[$chapter]->getName()."\n".$this->chapters[$chapter]->generate(); + $text = $this->getAsText($this->chapters[$chapter]); if ($outputDir) { $outputFile = rtrim($outputDir, '/').'/'.$chapter.'.md'; file_put_contents($outputFile, $text); @@ -85,4 +78,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + + private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = [1]): string + { + $chapter->setLevels($levels); + $text = ''; + $l = join('.', $levels); + if (null !== ($t = $chapter->getTitle())) { + $text .= '# '.$l.': '.$t."\n"; + } + if (null !== ($t = $chapter->getHeader())) { + $text .= $t."\n"; + } + if (null !== ($t = $chapter->getContent())) { + $text .= $t."\n"; + } + + $n = 1; + foreach ($chapter->getChildren() as $child) { + $subLevels = $levels; + $subLevels[] = $n++; + $text .= $this->getAsText($child, $subLevels); + } + + if (null !== ($t = $chapter->getFooter())) { + $text .= $t."\n"; + } + + return $text; + } } diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php new file mode 100644 index 000000000..7d626863e --- /dev/null +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -0,0 +1,41 @@ +levels = $levels; + } + + public function getLevels(): array + { + return $this->levels; + } + + public function getHeader(): ?string + { + return null; + } + + public function getContent(): ?string + { + return null; + } + + public function getFooter(): ?string + { + return null; + } + + /** DocumentationGeneratorInterface[] */ + public function getChildren(): array + { + return []; + } +} diff --git a/databox/api/src/Documentation/DocumentationGeneratorInterface.php b/databox/api/src/Documentation/DocumentationGeneratorInterface.php index 06b0f1b2d..bb8d2c278 100644 --- a/databox/api/src/Documentation/DocumentationGeneratorInterface.php +++ b/databox/api/src/Documentation/DocumentationGeneratorInterface.php @@ -4,14 +4,9 @@ namespace App\Documentation; -use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; - -#[AutoconfigureTag(self::TAG)] interface DocumentationGeneratorInterface { final public const string TAG = 'documentation_generator'; - public static function getName(): string; - - public function generate(): string; + public function getName(): string; } diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 0d6be4e7e..731b0cf88 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -6,14 +6,19 @@ use Symfony\Component\Yaml\Yaml; -class InitialValuesDocumentationGenerator implements DocumentationGeneratorInterface +class InitialValuesDocumentationGenerator extends DocumentationGenerator { - public static function getName(): string + public function getName(): string + { + return 'initial_attribute_values'; + } + + public function getTitle(): string { return 'Initial Attribute Values'; } - public function generate(): string + public function getContent(): ?string { $n = 0; $output = ''; @@ -26,7 +31,13 @@ public function generate(): string $output .= "\n---\n"; } - $output .= sprintf("## %s\n", $test['about']['title'] ?? ''); + $levels = $this->getLevels(); + $levels[] = $n; + + $output .= sprintf("## %s: %s\n", + join('.', $levels), + $test['about']['title'] ?? '' + ); if ($description = $test['about']['description'] ?? '') { $output .= sprintf("%s\n", $description); } diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index f985625cd..e1bc7f8a9 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -6,18 +6,23 @@ use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; -class RenditionBuilderDocumentationGenerator implements DocumentationGeneratorInterface +class RenditionBuilderDocumentationGenerator extends DocumentationGenerator { public function __construct(private RenditionBuilderConfigurationDocumentation $renditionBuilderConfigurationDocumentation) { } - public static function getName(): string + public function getName(): string + { + return 'rendition_factory'; + } + + public function getTitle(): string { return 'Rendition Factory'; } - public function generate(): string + public function getContent(): string { return $this->renditionBuilderConfigurationDocumentation->generate(); } diff --git a/databox/api/src/Documentation/RootDocumentationGenerator.php b/databox/api/src/Documentation/RootDocumentationGenerator.php new file mode 100644 index 000000000..b98773504 --- /dev/null +++ b/databox/api/src/Documentation/RootDocumentationGenerator.php @@ -0,0 +1,35 @@ +initialValuesDocumentationGenerator, + $this->renditionBuilderDocumentationGenerator, + ]; + } +} diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index 43230d2c3..7dd861088 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -1,25 +1,29 @@ # shortcuts # # - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. -# if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) +# if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) # - same principle applies for expected/ values (no locale == '_' locale) # # - expected values is an array, possibly with only one element -# if only one value is expected, the plain value can be used (it will test with [value]) +# if only one value is expected, the plain value can be used (it will test with [value]) # # - fieldType defaults to text # # - isMultiple defaults to false # # - possible fieldTypes are: -# boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text +# boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text +# +# - documentation generation +# Tests with an "about" section will be included in the documentation exiftoolVersion: about: - title: 'Vérification du fonctionnement de l''intégration metadata-read' + title: 'Check of integration "metadata-read"' description: - 'Cette lecture permet de s''assurer du bon déroulement du processus d''extraction des métadonnées. - La metadata `ExifTool:ExifToolVersion` est toujours retournée par exiftool.' + 'Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. + integration is functional. + The metadata `ExifTool:ExifToolVersion` in always returned by exiftool.' definitions: exiftoolVersion: initialValues: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' @@ -30,59 +34,59 @@ exiftoolVersion: multi2mono: about: - title: 'Meta multi-valuée -> attribut mono-valué (méthode "metadata")' + title: 'Multi-values metadata -> mono-value attribute ("metadata" method)' description: - 'Les valeurs seront séparées par des points-virgules.' + 'Values will be separated by " ; "' definitions: Keywords: isMultiple: false initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: 'chien ; chat ; oiseau' + Keywords: 'dog ; cat ; bird' multi2multi: about: - title: 'Meta multi-valuée -> attribut multi-valué (méthode "metadata")' + title: 'Multi-values metadata -> multi-values attribute ("metadata" method)' definitions: Keywords: isMultiple: true initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: ['chien', 'chat', 'oiseau'] + Keywords: ['dog', 'cat', 'bird'] multi2mono_template: about: - title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' - description: 'attention : utilisation erronée de getMetadata(...).value (sans "s")' + title: 'Multi-values metadata -> multi-values attribute ("template" method)' + description: '__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s")' definitions: Keywords: isMultiple: true initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: 'chien ; chat ; oiseau' + Keywords: 'dog ; cat ; bird' multi2multi_template: about: - title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' - description: 'Utilisation correcte de getMetadata(...).value__s__' + title: 'Multi-values metadata -> multi-values attribute ("template" method)' + description: 'Good usage of `getMetadata(...).values`' definitions: Keywords: isMultiple: true initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values | join(''\n'') }}"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: ['chien', 'chat', 'oiseau'] + Keywords: ['dog', 'cat', 'bird'] mono_multilingue: about: - title: 'mono multilingue' + title: 'Mono-value multilocale' definitions: Copyright: initialValues: @@ -97,7 +101,7 @@ mono_multilingue: multi_multilingue: about: - title: 'multi multilingue' + title: 'Multi-values multilocale' definitions: Keywords: isMultiple: true From 7b0b9a578f7997b079a89b64114e9fff36963e39 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Mon, 7 Jul 2025 15:27:26 +0200 Subject: [PATCH 09/28] add tests and allow empty metadata in tests --- .../InitialValuesDocumentationGenerator.php | 2 +- .../InitialAttributeValuesResolverTest.php | 5 +- .../InitialAttributeValuesResolverData.yaml | 111 ++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 731b0cf88..7cafec74e 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -70,7 +70,7 @@ public function getContent(): ?string $output .= "### ... with file metadata ...\n"; $output .= "| metadata | value(s) |\n"; $output .= "|---|---|\n"; - foreach ($test['metadata'] as $metadataName => $values) { + foreach ($test['metadata'] ?? [] as $metadataName => $values) { $v = is_array($values) ? $values : [$values]; $output .= sprintf("| %s | `%s` |\n", $metadataName, join('` ; `', $v)); } diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index c78b115fd..2777c8411 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -50,7 +50,7 @@ function ($test) { * * @param array $metadata */ - public function testResolveInitialAttributes(array $definitions, array $metadata, array $expected): void + public function testResolveInitialAttributes(array $definitions, ?array $metadata, array $expected): void { $attributeDefinitions = []; foreach ($definitions as $name => $definition) { @@ -145,6 +145,9 @@ private function isNumericArray($a): bool private function conformMetadata($data): array { + if (null === $data) { + return []; + } $conformed = []; $data = is_array($data) ? $data : [$data]; foreach ($data as $key => $value) { diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index 7dd861088..09d9f6631 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -116,3 +116,114 @@ multi_multilingue: en: ['dog', 'cat', 'bird'] fr: ['chien', 'chat', 'oiseau'] +unknown_tag: + about: + title: 'Unknown tag' + definitions: + zAttribute: + initialValues: '{ + "type": "template", + "value": "{{ (file.getMetadata(''badTag'').value ?? ''whaat ?'') }}" + }' + metadata: ~ + expected: + zAttribute: 'whaat ?' + +first_metadata: + about: + title: 'First metadata set, with alternate value (1)' + description: 'for this test, only the metadata `IPTC:City` is set.' + definitions: + City: + initialValues: '{ + "type": "template", + "value": "{{ file.getMetadata(''XMP-iptcCore:CreatorCity'').value ?? file.getMetadata(''IPTC:City'').value ?? ''no-city ?'' }}" + }' + metadata: + 'IPTC:City': 'Paris' + expected: + City: 'Paris' + +first_metadata_default: + about: + title: 'First metadata set, with alternate value (2)' + description: 'for this test, no metadata is set, so the default value is used.' + definitions: + City: + initialValues: '{ + "type": "template", + "value": "{{ file.getMetadata(''XMP-iptcCore:CreatorCity'').value ?? file.getMetadata(''IPTC:City'').value ?? ''no-city ?'' }}" + }' + metadata: ~ + expected: + City: 'no-city ?' + +many_metadata_to_mono: + about: + title: 'Many metadata to a mono-value attribute' + description: 'Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required.' + definitions: + CreatorLocation: + initialValues: '{ + "type": "template", + "value": "{{ [file.getMetadata(''XMP-iptcCore:CreatorCity'').value, file.getMetadata(''XMP-iptcCore:CreatorCountry'').value] | join('' - '') }}" + }' + metadata: + 'XMP-iptcCore:CreatorCity': 'Paris' + 'XMP-iptcCore:CreatorCountry': 'France' + expected: + CreatorLocation: 'Paris - France' + +many_metadata_to_multi: + about: + title: 'Many metadata to a multi-values attribute' + description: "_nb_: We fill an array that will be joined by `\\n` to generate __one line per item__ (required).\n + \n + The `merge` filter __requires__ arrays, so `IPTC:Keywords` defaults to `[]` in case there is no keywords in metadata.\n" + definitions: + Keywords: + isMultiple: true + initialValues: '{ + "type": "template", + "value": "{{ ( (file.getMetadata(''IPTC:Keywords'').values ?? []) | merge([file.getMetadata(''IPTC:City'').value, file.getMetadata(''XMP-photoshop:Country'').value ]) ) | join(''\n'') }}" + }' + metadata: + 'IPTC:Keywords': ['Eiffel Tower', 'Seine river'] + 'IPTC:City': 'Paris' + 'XMP-photoshop:Country': 'France' + expected: + Keywords: ['Eiffel Tower', 'Seine river', 'Paris', 'France'] + +code_to_string: + about: + title: 'Transform code to human readable text' + description: 'here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings.' + definitions: + ExposureProgram: + initialValues: '{ + "type": "template", + "value": "{{ [ + ''Not Defined'', ''Manual'', ''Program AE'', + ''Aperture-priority AE'', ''Shutter speed priority AE'', + ''Creative (Slow speed)'', ''Action (High speed)'', ''Portrait'', + ''Landscape'', ''Bulb'' + ][file.getMetadata(''ExifIFD:ExposureProgram'').value] + ?? ''Unknown mode'' + }}" + }' + metadata: + 'ExifIFD:ExposureProgram': 2 + expected: + ExposureProgram: 'Program AE' + +code_to_string_2: + definitions: + ExposureProgram: + initialValues: '{ + "type": "template", + "value": "{{ [ ''a'', ''b'' ][file.getMetadata(''ExifIFD:ExposureProgram'').value] ?? ''z'' }}" + }' + metadata: ~ + expected: + ExposureProgram: 'z' + From b0b6e4e8fe57b0f6de1bc3f8c7152ef24ad90539 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Jul 2025 16:47:00 +0200 Subject: [PATCH 10/28] move test data provider to src (because used to generate doc) ; add frontmatter header to generated md files ; generated md files can go to subdir --- .../Command/DocumentationDumperCommand.php | 28 +++++++++----- .../Documentation/DocumentationGenerator.php | 8 ++++ .../InitialAttributeValuesResolverData.yaml | 37 ++++++++++--------- .../InitialValuesDocumentationGenerator.php | 19 ++++++---- ...RenditionBuilderDocumentationGenerator.php | 5 +++ .../RootDocumentationGenerator.php | 35 ------------------ .../InitialAttributeValuesResolverTest.php | 5 ++- 7 files changed, 66 insertions(+), 71 deletions(-) rename databox/api/{tests/fixtures/metadata => src/Documentation}/InitialAttributeValuesResolverData.yaml (93%) delete mode 100644 databox/api/src/Documentation/RootDocumentationGenerator.php diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 88721f8e7..0aa5c0184 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -47,9 +47,9 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $outputDir = $input->getOption('output'); - if ($outputDir && !is_dir($outputDir)) { - $output->writeln(sprintf('Output directory "%s" does not exists.', $outputDir)); + $outputRoot = trim($input->getOption('output')); + if ($outputRoot && !is_dir($outputRoot)) { + $output->writeln(sprintf('Output directory "%s" does not exists.', $outputRoot)); return Command::FAILURE; } @@ -67,8 +67,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int } foreach ($input->getArgument('chapters') as $chapter) { $text = $this->getAsText($this->chapters[$chapter]); - if ($outputDir) { - $outputFile = rtrim($outputDir, '/').'/'.$chapter.'.md'; + if ($outputRoot) { + $outputDir = rtrim($outputRoot, '/'); + if ('' !== ($subDir = $this->chapters[$chapter]->getSubdirectory())) { + $outputDir .= '/'.trim($subDir, " \n\r\t\v\0/"); + } + @mkdir($outputDir, 0777, true); + $outputFile = $outputDir.'/'.$chapter.'.md'; file_put_contents($outputFile, $text); $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); } else { @@ -79,14 +84,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = [1]): string + private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = []): string { $chapter->setLevels($levels); $text = ''; $l = join('.', $levels); - if (null !== ($t = $chapter->getTitle())) { - $text .= '# '.$l.': '.$t."\n"; - } + + $title = $chapter->getTitle() ?? $chapter->getName(); + $text .= "---\n".$l.($l ? ': ' : '').$title."\n---\n\n"; + if (null !== ($t = $chapter->getHeader())) { $text .= $t."\n"; } @@ -97,7 +103,9 @@ private function getAsText(DocumentationGeneratorInterface $chapter, array $leve $n = 1; foreach ($chapter->getChildren() as $child) { $subLevels = $levels; - $subLevels[] = $n++; + if (!empty($subLevels)) { + $subLevels[] = $n++; + } $text .= $this->getAsText($child, $subLevels); } diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php index 7d626863e..095bbf724 100644 --- a/databox/api/src/Documentation/DocumentationGenerator.php +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -4,6 +4,9 @@ namespace App\Documentation; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +#[AutoconfigureTag(DocumentationGeneratorInterface::TAG)] abstract class DocumentationGenerator implements DocumentationGeneratorInterface { private array $levels = []; @@ -23,6 +26,11 @@ public function getHeader(): ?string return null; } + public function getSubdirectory(): string + { + return ''; + } + public function getContent(): ?string { return null; diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml similarity index 93% rename from databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml rename to databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml index 09d9f6631..8397fc95e 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml @@ -1,4 +1,10 @@ -# shortcuts +# this file is used to +# - generate the documentation for the initial values of attributes +# blocks with a "documentation" section will be included in the documentation +# - provide the data for the tests +# blocks with `test: false` will not be included in the tests +# +# syntax and shortcuts # # - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. # if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) @@ -13,12 +19,9 @@ # # - possible fieldTypes are: # boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text -# -# - documentation generation -# Tests with an "about" section will be included in the documentation exiftoolVersion: - about: + documentation: title: 'Check of integration "metadata-read"' description: 'Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. @@ -33,7 +36,7 @@ exiftoolVersion: exiftoolVersion: '12.42' multi2mono: - about: + documentation: title: 'Multi-values metadata -> mono-value attribute ("metadata" method)' description: 'Values will be separated by " ; "' @@ -47,7 +50,7 @@ multi2mono: Keywords: 'dog ; cat ; bird' multi2multi: - about: + documentation: title: 'Multi-values metadata -> multi-values attribute ("metadata" method)' definitions: Keywords: @@ -59,7 +62,7 @@ multi2multi: Keywords: ['dog', 'cat', 'bird'] multi2mono_template: - about: + documentation: title: 'Multi-values metadata -> multi-values attribute ("template" method)' description: '__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s")' definitions: @@ -72,7 +75,7 @@ multi2mono_template: Keywords: 'dog ; cat ; bird' multi2multi_template: - about: + documentation: title: 'Multi-values metadata -> multi-values attribute ("template" method)' description: 'Good usage of `getMetadata(...).values`' definitions: @@ -85,7 +88,7 @@ multi2multi_template: Keywords: ['dog', 'cat', 'bird'] mono_multilingue: - about: + documentation: title: 'Mono-value multilocale' definitions: Copyright: @@ -100,7 +103,7 @@ mono_multilingue: fr: '(c) Bob. Tous droits réservés' multi_multilingue: - about: + documentation: title: 'Multi-values multilocale' definitions: Keywords: @@ -117,7 +120,7 @@ multi_multilingue: fr: ['chien', 'chat', 'oiseau'] unknown_tag: - about: + documentation: title: 'Unknown tag' definitions: zAttribute: @@ -130,7 +133,7 @@ unknown_tag: zAttribute: 'whaat ?' first_metadata: - about: + documentation: title: 'First metadata set, with alternate value (1)' description: 'for this test, only the metadata `IPTC:City` is set.' definitions: @@ -145,7 +148,7 @@ first_metadata: City: 'Paris' first_metadata_default: - about: + documentation: title: 'First metadata set, with alternate value (2)' description: 'for this test, no metadata is set, so the default value is used.' definitions: @@ -159,7 +162,7 @@ first_metadata_default: City: 'no-city ?' many_metadata_to_mono: - about: + documentation: title: 'Many metadata to a mono-value attribute' description: 'Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required.' definitions: @@ -175,7 +178,7 @@ many_metadata_to_mono: CreatorLocation: 'Paris - France' many_metadata_to_multi: - about: + documentation: title: 'Many metadata to a multi-values attribute' description: "_nb_: We fill an array that will be joined by `\\n` to generate __one line per item__ (required).\n \n @@ -195,7 +198,7 @@ many_metadata_to_multi: Keywords: ['Eiffel Tower', 'Seine river', 'Paris', 'France'] code_to_string: - about: + documentation: title: 'Transform code to human readable text' description: 'here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings.' definitions: diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 7cafec74e..4d2f2f41c 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -18,12 +18,17 @@ public function getTitle(): string return 'Initial Attribute Values'; } + public function getSubdirectory(): string + { + return 'Databox/Attributes'; + } + public function getContent(): ?string { $n = 0; $output = ''; - foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { - if (!($test['about'] ?? false)) { + foreach (Yaml::parseFile(__DIR__.'/InitialAttributeValuesResolverData.yaml') as $example) { + if (!($example['documentation'] ?? false)) { continue; } @@ -36,14 +41,14 @@ public function getContent(): ?string $output .= sprintf("## %s: %s\n", join('.', $levels), - $test['about']['title'] ?? '' + $example['documentation']['title'] ?? '' ); - if ($description = $test['about']['description'] ?? '') { + if ($description = $example['documentation']['description'] ?? '') { $output .= sprintf("%s\n", $description); } $output .= "### Attribute(s) definition(s) ...\n"; - foreach ($test['definitions'] as $name => $definition) { + foreach ($example['definitions'] as $name => $definition) { $output .= sprintf("- __%s__: `%s` ; [%s] `multi` ; [%s] `translatable`\n", $name, $definition['fieldType'] ?? 'text', @@ -70,7 +75,7 @@ public function getContent(): ?string $output .= "### ... with file metadata ...\n"; $output .= "| metadata | value(s) |\n"; $output .= "|---|---|\n"; - foreach ($test['metadata'] ?? [] as $metadataName => $values) { + foreach ($example['metadata'] ?? [] as $metadataName => $values) { $v = is_array($values) ? $values : [$values]; $output .= sprintf("| %s | `%s` |\n", $metadataName, join('` ; `', $v)); } @@ -78,7 +83,7 @@ public function getContent(): ?string $output .= "\n"; $output .= "### ... set attribute(s) initial value(s)\n"; - $this->dumpExpected($output, $test['expected'], 1); + $this->dumpExpected($output, $example['expected'], 1); $output .= "\n"; } diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index e1bc7f8a9..2141d40f1 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -22,6 +22,11 @@ public function getTitle(): string return 'Rendition Factory'; } + public function getSubdirectory(): string + { + return 'Databox/Renditions'; + } + public function getContent(): string { return $this->renditionBuilderConfigurationDocumentation->generate(); diff --git a/databox/api/src/Documentation/RootDocumentationGenerator.php b/databox/api/src/Documentation/RootDocumentationGenerator.php deleted file mode 100644 index b98773504..000000000 --- a/databox/api/src/Documentation/RootDocumentationGenerator.php +++ /dev/null @@ -1,35 +0,0 @@ -initialValuesDocumentationGenerator, - $this->renditionBuilderDocumentationGenerator, - ]; - } -} diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 2777c8411..10d3670d9 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -37,9 +37,10 @@ function ($test) { ]; }, array_filter( - Yaml::parseFile(__DIR__.'/../../fixtures/metadata/InitialAttributeValuesResolverData.yaml'), + // the data file is used for documentation generation AND as a data provider for tests + Yaml::parseFile(__DIR__.'/../../../src/Documentation/InitialAttributeValuesResolverData.yaml'), function ($test) { - return $test['enabled'] ?? true; + return $test['test'] ?? true; } ) ); From c46925cca57bc4329a72f2e453565b442f1637c1 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Jul 2025 19:06:18 +0200 Subject: [PATCH 11/28] remove app:documentation:dump options --- .../Command/DocumentationDumperCommand.php | 57 +++++++------------ .../Documentation/DocumentationGenerator.php | 5 -- .../InitialValuesDocumentationGenerator.php | 7 +-- ...RenditionBuilderDocumentationGenerator.php | 7 +-- 4 files changed, 21 insertions(+), 55 deletions(-) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 0aa5c0184..a16b0c27c 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -7,7 +7,6 @@ use App\Documentation\DocumentationGeneratorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; @@ -39,46 +38,22 @@ protected function configure(): void $this ->setDescription('Dump code-generated documentation(s)') - ->addArgument('chapters', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Chapter(s) to dump. If not specified, all chapters will be dumped.') - ->addOption('output', 'o', InputArgument::OPTIONAL, 'Output directory to write the documentation to. If not specified, it will be written to stdout.') - ->setHelp(sprintf('chapters: "%s"', join('", "', array_keys($this->chapters)))) + ->setHelp(sprintf('chapters: %s', join(' ; ', array_keys($this->chapters)))) ; } protected function execute(InputInterface $input, OutputInterface $output): int { - $outputRoot = trim($input->getOption('output')); - if ($outputRoot && !is_dir($outputRoot)) { - $output->writeln(sprintf('Output directory "%s" does not exists.', $outputRoot)); + foreach (array_keys($this->chapters) as $chapter) { + $pathParts = explode('/', trim($this->chapters[$chapter]->getName(), " \n\r\t\v\0/")); + $filename = array_pop($pathParts); - return Command::FAILURE; - } - - foreach ($input->getArgument('chapters') as $chapter) { - if (!isset($this->chapters[$chapter])) { - $output->writeln(sprintf('Unknown chapter "%s". Available chapters are "%s"', $chapter, join('", "', array_keys($this->chapters)))); + $outputDir = '../../doc/'.join('/', $pathParts); + @mkdir($outputDir, 0777, true); + $outputFile = $outputDir.'/'.$filename.'.md'; - return Command::FAILURE; - } - } - - if (empty($input->getArgument('chapters'))) { - $input->setArgument('chapters', array_keys($this->chapters)); - } - foreach ($input->getArgument('chapters') as $chapter) { - $text = $this->getAsText($this->chapters[$chapter]); - if ($outputRoot) { - $outputDir = rtrim($outputRoot, '/'); - if ('' !== ($subDir = $this->chapters[$chapter]->getSubdirectory())) { - $outputDir .= '/'.trim($subDir, " \n\r\t\v\0/"); - } - @mkdir($outputDir, 0777, true); - $outputFile = $outputDir.'/'.$chapter.'.md'; - file_put_contents($outputFile, $text); - $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); - } else { - $output->writeln($text); - } + file_put_contents($outputFile, $this->getAsText($this->chapters[$chapter])); + $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); } return Command::SUCCESS; @@ -87,11 +62,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = []): string { $chapter->setLevels($levels); - $text = ''; - $l = join('.', $levels); - $title = $chapter->getTitle() ?? $chapter->getName(); - $text .= "---\n".$l.($l ? ': ' : '').$title."\n---\n\n"; + $title = str_replace("'", "''", $chapter->getTitle() ?? $chapter->getName()); // Escape single quotes for YAML frontmatter + if (!empty($levels)) { + $title = join('.', $levels).': '.$title; + } + $frontmatter = "---\n" + ."title: '".$title."'\n" + ."comment: 'Generated by the DocumentationDumperCommand, do not modify'\n" + ."---\n"; + + $text = $frontmatter."\n"; if (null !== ($t = $chapter->getHeader())) { $text .= $t."\n"; diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php index 095bbf724..118319336 100644 --- a/databox/api/src/Documentation/DocumentationGenerator.php +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -26,11 +26,6 @@ public function getHeader(): ?string return null; } - public function getSubdirectory(): string - { - return ''; - } - public function getContent(): ?string { return null; diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 4d2f2f41c..f949a9ff4 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -10,7 +10,7 @@ class InitialValuesDocumentationGenerator extends DocumentationGenerator { public function getName(): string { - return 'initial_attribute_values'; + return 'Databox/Attributes/initial_attribute_values'; } public function getTitle(): string @@ -18,11 +18,6 @@ public function getTitle(): string return 'Initial Attribute Values'; } - public function getSubdirectory(): string - { - return 'Databox/Attributes'; - } - public function getContent(): ?string { $n = 0; diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index 2141d40f1..a8b0826b9 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -14,7 +14,7 @@ public function __construct(private RenditionBuilderConfigurationDocumentation $ public function getName(): string { - return 'rendition_factory'; + return 'Databox/Renditions/rendition_factory'; } public function getTitle(): string @@ -22,11 +22,6 @@ public function getTitle(): string return 'Rendition Factory'; } - public function getSubdirectory(): string - { - return 'Databox/Renditions'; - } - public function getContent(): string { return $this->renditionBuilderConfigurationDocumentation->generate(); From baf45422483bc082bd99a60e070ff80d181a893e Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Jul 2025 19:41:39 +0200 Subject: [PATCH 12/28] add generated doc to git --- .../Attributes/initial_attribute_values.md | 350 ++++++++++++ doc/Databox/Renditions/rendition_factory.md | 531 ++++++++++++++++++ 2 files changed, 881 insertions(+) create mode 100644 doc/Databox/Attributes/initial_attribute_values.md create mode 100644 doc/Databox/Renditions/rendition_factory.md diff --git a/doc/Databox/Attributes/initial_attribute_values.md b/doc/Databox/Attributes/initial_attribute_values.md new file mode 100644 index 000000000..9790b60d1 --- /dev/null +++ b/doc/Databox/Attributes/initial_attribute_values.md @@ -0,0 +1,350 @@ +--- +title: 'Initial Attribute Values' +comment: 'Generated by the DocumentationDumperCommand, do not modify' +--- + +## 1: Check of integration "metadata-read" +Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. integration is functional. The metadata `ExifTool:ExifToolVersion` in always returned by exiftool. +### Attribute(s) definition(s) ... +- __exiftoolVersion__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "metadata", + "value": "ExifTool:ExifToolVersion" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| ExifTool:ExifToolVersion | `12.42` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | exiftoolVersion | `12.42` + + +--- +## 2: Multi-values metadata -> mono-value attribute ("metadata" method) +Values will be separated by " ; " +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "metadata", + "value": "IPTC:Keywords" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords | `dog ; cat ; bird` + + +--- +## 3: Multi-values metadata -> multi-values attribute ("metadata" method) +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "metadata", + "value": "IPTC:Keywords" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords #1 | `dog` | + | Keywords #2 | `cat` | + | Keywords #3 | `bird` | + + +--- +## 4: Multi-values metadata -> multi-values attribute ("template" method) +__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s") +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('IPTC:Keywords').value }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords | `dog ; cat ; bird` + + +--- +## 5: Multi-values metadata -> multi-values attribute ("template" method) +Good usage of `getMetadata(...).values` +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('IPTC:Keywords').values | join('\n') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords #1 | `dog` | + | Keywords #2 | `cat` | + | Keywords #3 | `bird` | + + +--- +## 6: Mono-value multilocale +### Attribute(s) definition(s) ... +- __Copyright__: `text` ; [ ] `multi` ; [ ] `translatable` + + - locale `en` + ```json + { + "type": "template", + "value": "(c) {{ file.getMetadata('XMP-dc:Creator').value }}. All rights reserved" + } + ``` + - locale `fr` + ```json + { + "type": "template", + "value": "(c) {{ file.getMetadata('XMP-dc:Creator').value }}. Tous droits réservés" + } + ``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| XMP-dc:Creator | `Bob` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | __locale `en`__ | | + | Copyright | `(c) Bob. All rights reserved` | + | __locale `fr`__ | | + | Copyright | `(c) Bob. Tous droits réservés` | + + +--- +## 7: Multi-values multilocale +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + + - locale `en` + ```json + { + "type": "metadata", + "value": "XMP-dc:Subject" + } + ``` + - locale `fr` + ```json + { + "type": "metadata", + "value": "IPTC:SupplementalCategories" + } + ``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| XMP-dc:Subject | `dog` ; `cat` ; `bird` | +| IPTC:SupplementalCategories | `chien` ; `chat` ; `oiseau` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | __locale `en`__ | | + | Keywords #1 | `dog` | + | Keywords #2 | `cat` | + | Keywords #3 | `bird` | + | __locale `fr`__ | | + | Keywords #1 | `chien` | + | Keywords #2 | `chat` | + | Keywords #3 | `oiseau` | + + +--- +## 8: Unknown tag +### Attribute(s) definition(s) ... +- __zAttribute__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ (file.getMetadata('badTag').value ?? 'whaat ?') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | zAttribute | `whaat ?` + + +--- +## 9: First metadata set, with alternate value (1) +for this test, only the metadata `IPTC:City` is set. +### Attribute(s) definition(s) ... +- __City__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('XMP-iptcCore:CreatorCity').value ?? file.getMetadata('IPTC:City').value ?? 'no-city ?' }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:City | `Paris` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | City | `Paris` + + +--- +## 10: First metadata set, with alternate value (2) +for this test, no metadata is set, so the default value is used. +### Attribute(s) definition(s) ... +- __City__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('XMP-iptcCore:CreatorCity').value ?? file.getMetadata('IPTC:City').value ?? 'no-city ?' }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | City | `no-city ?` + + +--- +## 11: Many metadata to a mono-value attribute +Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required. +### Attribute(s) definition(s) ... +- __CreatorLocation__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ [file.getMetadata('XMP-iptcCore:CreatorCity').value, file.getMetadata('XMP-iptcCore:CreatorCountry').value] | join(' - ') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| XMP-iptcCore:CreatorCity | `Paris` | +| XMP-iptcCore:CreatorCountry | `France` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | CreatorLocation | `Paris - France` + + +--- +## 12: Many metadata to a multi-values attribute +_nb_: We fill an array that will be joined by `\n` to generate __one line per item__ (required). + + The `merge` filter __requires__ arrays, so `IPTC:Keywords` defaults to `[]` in case there is no keywords in metadata. + +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ ( (file.getMetadata('IPTC:Keywords').values ?? []) | merge([file.getMetadata('IPTC:City').value, file.getMetadata('XMP-photoshop:Country').value ]) ) | join('\n') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `Eiffel Tower` ; `Seine river` | +| IPTC:City | `Paris` | +| XMP-photoshop:Country | `France` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords #1 | `Eiffel Tower` | + | Keywords #2 | `Seine river` | + | Keywords #3 | `Paris` | + | Keywords #4 | `France` | + + +--- +## 13: Transform code to human readable text +here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings. +### Attribute(s) definition(s) ... +- __ExposureProgram__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ [ 'Not Defined', 'Manual', 'Program AE', 'Aperture-priority AE', 'Shutter speed priority AE', 'Creative (Slow speed)', 'Action (High speed)', 'Portrait', 'Landscape', 'Bulb' ][file.getMetadata('ExifIFD:ExposureProgram').value] ?? 'Unknown mode' }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| ExifIFD:ExposureProgram | `2` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | ExposureProgram | `Program AE` + + diff --git a/doc/Databox/Renditions/rendition_factory.md b/doc/Databox/Renditions/rendition_factory.md new file mode 100644 index 000000000..5b007ad9e --- /dev/null +++ b/doc/Databox/Renditions/rendition_factory.md @@ -0,0 +1,531 @@ +--- +title: 'Rendition Factory' +comment: 'Generated by the DocumentationDumperCommand, do not modify' +--- + +## `imagine` transformer module +Transform an image with some filter. +```yaml +- + module: imagine # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Output image format + format: ~ # Example: jpeg + # Filters to apply to the image + filters: + # Filter performs sizing transformations (specifically relative resizing) + relative_resize: + heighten: ~ # Example: 'value "60" => given 50x40px, output 75x60px using "heighten" option' + widen: ~ # Example: 'value "32" => given 50x40px, output 32x26px using "widen" option' + increase: ~ # Example: 'value "10" => given 50x40px, output 60x50px, using "increase" option' + scale: ~ # Example: 'value "2.5" => given 50x40px, output 125x100px using "scale" option' + # use and setup the resize filter + resize: + # set the size of the resizing area [width,height] + size: + 0: ~ # Example: '120' + 1: ~ # Example: '90' + # Filter performs thumbnail transformations (which includes scaling and potentially cropping operations) + thumbnail: + # set the thumbnail size to [width,height] pixels + size: + 0: ~ # Example: '32' + 1: ~ # Example: '32' + # Sets the desired resize method: "outbound" crops the image as required, while "inset" performs a non-cropping relative resize. + mode: ~ # Example: inset + # Toggles allowing image up-scaling when the image is smaller than the desired thumbnail size. Value: true or false + allow_upscale: ~ + # filter performs sizing transformations (which includes cropping operations) + crop: + # set the size of the cropping area [width,height] + size: + 0: ~ # Example: '300' + 1: ~ # Example: '600' + # Sets the top, left-post anchor coordinates where the crop operation starts[x, y] + start: + 0: ~ # Example: '32' + 1: ~ # Example: '160' + # filter adds a watermark to an existing image + watermark: + # Path to the watermark image + image: ~ + # filter fill background color + background_fill: + # Sets the background color HEX value. The default color is white (#fff). + color: ~ + # Sets the background opacity. The value should be within a range of 0 (fully transparent) - 100 (opaque). default opacity 100 + opacity: ~ + # filter performs file transformations (which includes metadata removal) + strip: ~ + # filter performs sizing transformations (specifically image scaling) + scale: + # Sets the "desired dimensions" [width, height], from which a relative resize is performed within these constraints. + dim: + 0: ~ # Example: '800' + 1: ~ # Example: '1000' + # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. + to: ~ # Example: '1.5' + # filter performs sizing transformations (specifically image up-scaling) + upscale: + # Sets the "desired min dimensions" [width, height], from which an up-scale is performed to meet the passed constraints. + min: + 0: ~ # Example: '1200' + 1: ~ # Example: '800' + # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. + by: ~ # Example: '0.7' + # filter performs sizing transformations (specifically image down-scaling) + downscale: + # Sets the "desired max dimensions" [width, height], from which a down-scale is performed to meet the passed constraints + max: + 0: ~ # Example: '1980' + 1: ~ # Example: '1280' + # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. + by: ~ # Example: '0.6' + # filter performs orientation transformations (which includes rotating the image) + auto_rotate: ~ + # filter performs orientation transformations (specifically image rotation) + rotate: + # Sets the rotation angle in degrees. The default value is 0. + angle: ~ # Example: '90' + # filter performs orientation transformations (specifically image flipping) + flip: + # Sets the "flip axis" that defines the axis on which to flip the image. Valid values: x, horizontal, y, vertical + axis: ~ # Example: x + # filter performs file transformations (which includes modifying the encoding method) + interlace: + # Sets the interlace mode to encode the file with. Valid values: none, line, plane, and partition. + mode: ~ # Example: line + # filter provides a resampling transformation by allows you to change the resolution of an image + resample: + # Sets the unit to use for pixel density, either "pixels per inch" or "pixels per centimeter". Valid values: ppi and ppc + unit: ~ # Example: ppi + # Sets the horizontal resolution in the specified unit + x: ~ # Example: '300' + # Sets the vertical resolution in the specified unit + y: ~ # Example: '200' + # Sets the optional temporary work directory. This filter requires a temporary location to save out and read back in the image binary, as these operations are requires to resample an image. By default, it is set to the value of the sys_get_temp_dir() function + tmp_dir: ~ # Example: /my/custom/temporary/directory/path + # filter performs thumbnail transformations (which includes scaling and potentially cropping operations) + fixed: + # Sets the "desired width" which initiates a proportional scale operation that up- or down-scales until the image width matches this value. + width: ~ # Example: '120' + # Sets the "desired height" which initiates a proportional scale operation that up- or down-scales until the image height matches this value + height: ~ # Example: '90' + # use stamp + stamp: + # path to the font file ttf + font: ~ + # available position value: topleft, top, topright, left, center, right, bottomleft, bottom, bottomright, under, above + position: ~ + angle: 0 + # font size + size: 16 + color: '#000000' + # the font alpha value + alpha: 100 + # text to stamp + text: ~ + # text width + width: ~ + # text background, option use for position under or above + background: '#FFFFFF' + # background transparancy, option use for position under or above + transparency: null +``` +## `void` transformer module +A module that does nothing (testing purpose) +```yaml +- + module: void # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true +``` +## `video_summary` transformer module +Assemble multiple extracts (clips) of the video. +```yaml +- + module: video_summary # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Skip video start, in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' + # Extract one video clip every period, in seconds or timecode + period: ~ # Required, Example: '5 ; "00:00:05.00"' + # Duration of each clip, in seconds or timecode + duration: ~ # Required, Example: '0.25 ; "00:00:00.25"' + # output format + format: ~ # Required, Example: video-mpeg + # extension of the output file + extension: 'default extension from format' # Example: mpeg + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| video |||| +|| video-mkv | video/x-matroska | mkv | +|| video-mpeg4 | video/mp4 | mp4 | +|| video-mpeg | video/mpeg | mpeg | +|| video-quicktime | video/quicktime | mov | +|| video-webm | video/webm | webm | +|| video-ogg | video/ogg | ogv | + +## `ffmpeg` transformer module +apply filters to a video using FFMpeg. +```yaml +- + module: ffmpeg # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # output format + format: ~ + # extension of the output file + extension: ~ + # Change the default video codec used by the output format + video_codec: ~ + # Change the default audio codec used by the output format + audio_codec: ~ + # Change the default video_kilobitrate used by the output format + video_kilobitrate: ~ + # Change the default audio_kilobitrate used by the output format + audio_kilobitrate: ~ + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ + # Filters to apply to the video + filters: + # Prototype: see list of available filters below + - + # Name of the filter + name: ~ # Required + # Whether to enable the filter + enabled: true +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| audio |||| +|| audio-wav | audio/wav | wav | +|| audio-aac | audio/aac | aac, m4a | +|| audio-mp3 | audio/mp3 | mp3 | +|| audio-ogg | audio/ogg | oga, ogg | +| image |||| +|| image-jpeg | image/jpeg | jpg, jpeg | +|| image-gif | image/gif | gif | +|| image-png | image/png | png | +|| image-tiff | image/tiff | tif, tiff | +| video |||| +|| video-mkv | video/x-matroska | mkv | +|| video-mpeg4 | video/mp4 | mp4 | +|| video-mpeg | video/mpeg | mpeg | +|| video-quicktime | video/quicktime | mov | +|| video-webm | video/webm | webm | +|| video-ogg | video/ogg | ogv | +### List of `ffmpeg` filters: +- `pre_clip` filter +```yaml +# Clip the video before applying other filters +- + name: pre_clip # Required + enabled: true + # Offset of frame in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.500" ; "{{ attr.start }}"' + # Duration in seconds or timecode + duration: null # Example: '30 ; "00:00:30" ; "{{ input.duration/2 }}"' +``` +- `clip` filter +```yaml +# Clip the video or audio +- + name: clip # Required + enabled: true + # Offset of frame in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.500" ; "{{ attr.start }}"' + # Duration in seconds or timecode + duration: null # Example: '30 ; "00:00:30" ; "{{ input.duration/2 }}"' +``` +- `remove_audio` filter +```yaml +# Remove the audio from the video +- + name: remove_audio # Required + enabled: true +``` +- `resample_audio` filter +```yaml +# Resample the audio +- + name: resample_audio # Required + enabled: true + rate: '44100' # Required +``` +- `resize` filter +```yaml +# Resize the video +- + name: resize # Required + enabled: true + # Width of the video + width: ~ # Required + # Height of the video + height: ~ # Required + # Resize mode + mode: inset # Example: inset + # Correct the width/height to the closest "standard" size + force_standards: true +``` +- `rotate` filter +```yaml +# Rotate the video +- + name: rotate # Required + enabled: true + # Angle of rotation [0 | 90 | 180 | 270] + angle: ~ # Required, Example: '90' +``` +- `pad` filter +```yaml +# Pad the video +- + name: pad # Required + enabled: true + # Width of the video + width: ~ # Required + # Height of the video + height: ~ # Required +``` +- `crop` filter +```yaml +# Crop the video +- + name: crop # Required + enabled: true + # X coordinate + x: ~ # Required + # Y coordinate + y: ~ # Required + # Width of the video + width: ~ # Required + # Height of the video + height: ~ # Required +``` +- `watermark` filter +```yaml +# Apply a watermark on the video +- + name: watermark # Required + enabled: true + # "relative" or "absolute" position + position: ~ # Required + # Path to the watermark image + path: ~ # Required + # top coordinate (only if position is "relative", set top OR bottom) + top: ~ + # bottom coordinate (only if position is "relative", set top OR bottom) + bottom: ~ + # left coordinate (only if position is "relative", set left OR right) + left: ~ + # right coordinate (only if position is "relative", set left OR right) + right: ~ + # X coordinate (only if position is "absolute") + x: ~ + # Y coordinate (only if position is "absolute") + y: ~ +``` +- `framerate` filter +```yaml +# Change the framerate +- + name: framerate # Required + enabled: true + # framerate + framerate: ~ # Required + # gop + gop: ~ +``` +- `synchronize` filter +```yaml +# re-synchronize audio and video +- + name: synchronize # Required + enabled: true +``` +## `video_to_frame` transformer module +Extract one frame from the video. +```yaml +- + module: video_to_frame # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Offset of frame in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' + # output format + format: ~ # Required, Example: image-jpeg + # extension of the output file + extension: 'default extension from format' # Example: jpg + # Change the quality of the output file (0-100) + quality: 80 + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| image |||| +|| image-jpeg | image/jpeg | jpg, jpeg | +|| image-gif | image/gif | gif | +|| image-png | image/png | png | +|| image-tiff | image/tiff | tif, tiff | + +## `video_to_animation` transformer module +Converts a video to an animated GIF / PNG. +```yaml +- + module: video_to_animation # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Start time in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' + # Duration in seconds or timecode + duration: null # Example: '30 ; "00:00:30.00" ; "{{ input.duration/2 }}"' + # Frames per second + fps: 1 + # Width in pixels + width: null + # Height in pixels + height: null + # Resize mode + mode: inset # One of "inset" + # output format + format: ~ # Required, Example: animated-png + # extension of the output file + extension: 'default extension from format' # Example: apng + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| animation |||| +|| animated-gif | image/gif | gif | +|| animated-png | image/apng | apng, png | +|| animated-webp | image/webp | webp | + +## `album_artwork` transformer module +Extract the artwork (cover) of an audio file. +```yaml +- + module: album_artwork # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # output format + format: ~ # Required, Example: image-jpeg + # extension of the output file + extension: 'default extension from format' # Example: jpg +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| image |||| +|| image-jpeg | image/jpeg | jpg, jpeg | +|| image-gif | image/gif | gif | +|| image-png | image/png | png | +|| image-tiff | image/tiff | tif, tiff | + +## `document_to_pdf` transformer module +Convert any document to PDF format. +```yaml +- + module: document_to_pdf # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: [] +``` +## `pdf_to_image` transformer module +Convert the first page of a PDF to an image. +```yaml +- + module: pdf_to_image # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Output image extension: jpg, jpeg, png, or webp + extension: jpeg + # Resolution of the output image in dpi + resolution: 300 + # Quality of the output image, from 0 to 100 + quality: 100 + # Size of the output image, [width, height] in pixels + size: + # Width of the output image in pixels + 0: ~ # Example: '150' + # Height of the output image in pixels + 1: ~ # Example: '100' +``` +## `set_dpi` transformer module +Change the dpi metadata of an image (no resampling). +```yaml +- + module: set_dpi # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + dpi: ~ # Required, Example: '72' +``` +## `download` transformer module +Download a file to be used as output. +```yaml +- + module: download # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # url of the file to download + url: ~ +``` + From 377b7b99fa46eff1a7aa9632bc019eefb4fdeeb0 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Tue, 22 Jul 2025 10:32:59 +0200 Subject: [PATCH 13/28] apply review --- .../api/src/Attribute/AttributeAssigner.php | 2 +- .../Command/DocumentationDumperCommand.php | 19 +++++++------- .../Documentation/DocumentationGenerator.php | 5 +--- .../DocumentationGeneratorInterface.php | 18 ++++++++++++- .../InitialAttributeValuesResolverData.yaml | 4 +-- .../InitialValuesDocumentationGenerator.php | 2 +- ...RenditionBuilderDocumentationGenerator.php | 2 +- .../InitialAttributeValuesResolverTest.php | 26 +++++++++---------- 8 files changed, 46 insertions(+), 32 deletions(-) diff --git a/databox/api/src/Attribute/AttributeAssigner.php b/databox/api/src/Attribute/AttributeAssigner.php index 8135e9645..af2cd03e9 100644 --- a/databox/api/src/Attribute/AttributeAssigner.php +++ b/databox/api/src/Attribute/AttributeAssigner.php @@ -10,7 +10,7 @@ use App\Entity\Core\AbstractBaseAttribute; use App\Entity\Core\Attribute; -class AttributeAssigner +final readonly class AttributeAssigner { public function __construct(private AttributeTypeRegistry $attributeTypeRegistry) { diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index a16b0c27c..dd88c87ea 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -29,11 +29,11 @@ protected function configure(): void /** @var DocumentationGeneratorInterface $documentation */ foreach ($this->documentations as $documentation) { - $name = $documentation->getName(); - if (isset($this->chapters[$name])) { - throw new \LogicException(sprintf('Chapter "%s" is already registered.', $name)); + $k = $documentation->getPath(); + if (isset($this->chapters[$k])) { + throw new \LogicException(sprintf('Chapter "%s" is already registered.', $k)); } - $this->chapters[$name] = $documentation; + $this->chapters[$k] = $documentation; } $this @@ -44,16 +44,17 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - foreach (array_keys($this->chapters) as $chapter) { - $pathParts = explode('/', trim($this->chapters[$chapter]->getName(), " \n\r\t\v\0/")); + foreach ($this->chapters as $chapter) { + $title = $chapter->getTitle() ?? $chapter->getPath(); + $pathParts = explode('/', trim($chapter->getPath(), " \n\r\t\v\0/")); $filename = array_pop($pathParts); $outputDir = '../../doc/'.join('/', $pathParts); @mkdir($outputDir, 0777, true); $outputFile = $outputDir.'/'.$filename.'.md'; - file_put_contents($outputFile, $this->getAsText($this->chapters[$chapter])); - $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); + file_put_contents($outputFile, $this->getAsText($chapter)); + $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $title, $outputFile)); } return Command::SUCCESS; @@ -63,7 +64,7 @@ private function getAsText(DocumentationGeneratorInterface $chapter, array $leve { $chapter->setLevels($levels); - $title = str_replace("'", "''", $chapter->getTitle() ?? $chapter->getName()); // Escape single quotes for YAML frontmatter + $title = str_replace("'", "''", $chapter->getTitle() ?? $chapter->getPath()); // Escape single quotes for YAML frontmatter if (!empty($levels)) { $title = join('.', $levels).': '.$title; } diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php index 118319336..1683ce834 100644 --- a/databox/api/src/Documentation/DocumentationGenerator.php +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -4,9 +4,6 @@ namespace App\Documentation; -use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; - -#[AutoconfigureTag(DocumentationGeneratorInterface::TAG)] abstract class DocumentationGenerator implements DocumentationGeneratorInterface { private array $levels = []; @@ -16,7 +13,7 @@ final public function setLevels(array $levels): void $this->levels = $levels; } - public function getLevels(): array + final public function getLevels(): array { return $this->levels; } diff --git a/databox/api/src/Documentation/DocumentationGeneratorInterface.php b/databox/api/src/Documentation/DocumentationGeneratorInterface.php index bb8d2c278..e4ce7fddd 100644 --- a/databox/api/src/Documentation/DocumentationGeneratorInterface.php +++ b/databox/api/src/Documentation/DocumentationGeneratorInterface.php @@ -4,9 +4,25 @@ namespace App\Documentation; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +#[AutoconfigureTag(DocumentationGeneratorInterface::TAG)] interface DocumentationGeneratorInterface { final public const string TAG = 'documentation_generator'; - public function getName(): string; + public function getPath(): string; + + public function setLevels(array $levels): void; + + public function getLevels(): array; + + public function getHeader(): ?string; + + public function getContent(): ?string; + + public function getFooter(): ?string; + + /** DocumentationGeneratorInterface[] */ + public function getChildren(): array; } diff --git a/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml index 8397fc95e..79d55dbca 100644 --- a/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml +++ b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml @@ -1,10 +1,10 @@ -# this file is used to +# This file is used to: # - generate the documentation for the initial values of attributes # blocks with a "documentation" section will be included in the documentation # - provide the data for the tests # blocks with `test: false` will not be included in the tests # -# syntax and shortcuts +# Syntax and shortcuts # # - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. # if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index f949a9ff4..7631302f6 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -8,7 +8,7 @@ class InitialValuesDocumentationGenerator extends DocumentationGenerator { - public function getName(): string + public function getPath(): string { return 'Databox/Attributes/initial_attribute_values'; } diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index a8b0826b9..4e323b8e8 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -12,7 +12,7 @@ public function __construct(private RenditionBuilderConfigurationDocumentation $ { } - public function getName(): string + public function getPath(): string { return 'Databox/Renditions/rendition_factory'; } diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 10d3670d9..4d5c4e0ca 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -81,7 +81,7 @@ public function testResolveInitialAttributes(array $definitions, ?array $metadat $fileMock->expects($this->any()) ->method('getMetadata') - ->willReturn($this->conformMetadata($metadata)); + ->willReturn($this->normalizeMetadata($metadata)); $assetMock = $this->createMock(Asset::class); $assetMock->expects($this->any()) @@ -101,20 +101,20 @@ public function testResolveInitialAttributes(array $definitions, ?array $metadat $result[$attribute->getDefinition()->getName()][$attribute->getLocale()][] = $attribute->getValue(); } - $this->assertEquals($this->conformExpected($expected), $result); + $this->assertEquals($this->normalizeExpected($expected), $result); } - private function conformExpected(array $expected): array + private function normalizeExpected(array $expected): array { - $conformed = []; + $normalized = []; foreach ($expected as $attributeName => $value) { if (is_array($value)) { if ($this->isNumericArray($value)) { // a simple list of values - $conformed[$attributeName] = ['_' => $value]; + $normalized[$attributeName] = ['_' => $value]; } else { // an array with key=locale - $conformed[$attributeName] = array_map( + $normalized[$attributeName] = array_map( function ($v) { return is_array($v) ? $v : [$v]; }, @@ -123,11 +123,11 @@ function ($v) { } } else { // a single value - $conformed[$attributeName] = ['_' => [$value]]; + $normalized[$attributeName] = ['_' => [$value]]; } } - return $conformed; + return $normalized; } private function isNumericArray($a): bool @@ -144,27 +144,27 @@ private function isNumericArray($a): bool return true; } - private function conformMetadata($data): array + private function normalizeMetadata($data): array { if (null === $data) { return []; } - $conformed = []; + $normalized = []; $data = is_array($data) ? $data : [$data]; foreach ($data as $key => $value) { if (is_array($value)) { - $conformed[$key] = [ + $normalized[$key] = [ 'value' => join(' ; ', $value), 'values' => $value, ]; } else { - $conformed[$key] = [ + $normalized[$key] = [ 'value' => $value, 'values' => [$value], ]; } } - return $conformed; + return $normalized; } } From a3fd199f0ee1e00a6fee9f6b001c17495c869f22 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 25 Jun 2025 21:02:20 +0200 Subject: [PATCH 14/28] wip --- .../InitialAttributeValuesResolver.php | 8 +- .../api/src/Attribute/AttributeAssigner.php | 2 +- .../InitialAttributeValuesResolverTest.php | 144 ++++++++++++++++++ 3 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php diff --git a/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php b/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php index ae2327084..270d0a308 100644 --- a/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php +++ b/databox/api/src/Asset/Attribute/InitialAttributeValuesResolver.php @@ -12,7 +12,6 @@ use App\Entity\Core\AttributeDefinition; use App\File\FileMetadataAccessorWrapper; use App\Repository\Core\AttributeDefinitionRepository; -use Doctrine\ORM\EntityManagerInterface; use Twig\Environment; use Twig\Loader\ArrayLoader; @@ -21,7 +20,7 @@ class InitialAttributeValuesResolver private readonly Environment $twig; public function __construct( - private readonly EntityManagerInterface $em, + private readonly AttributeDefinitionRepository $attributeDefinitionRepository, private readonly AttributeAssigner $attributeAssigner, ) { $this->twig = new Environment(new ArrayLoader(), [ @@ -36,10 +35,7 @@ public function resolveInitialAttributes(Asset $asset): array { $attributes = []; - /** @var AttributeDefinitionRepository $repo */ - $repo = $this->em->getRepository(AttributeDefinition::class); - - $definitions = $repo->getWorkspaceInitializeDefinitions($asset->getWorkspaceId()); + $definitions = $this->attributeDefinitionRepository->getWorkspaceInitializeDefinitions($asset->getWorkspaceId()); $fileMetadataAccessorWrapper = new FileMetadataAccessorWrapper($asset->getSource()); foreach ($definitions as $definition) { diff --git a/databox/api/src/Attribute/AttributeAssigner.php b/databox/api/src/Attribute/AttributeAssigner.php index af2cd03e9..8135e9645 100644 --- a/databox/api/src/Attribute/AttributeAssigner.php +++ b/databox/api/src/Attribute/AttributeAssigner.php @@ -10,7 +10,7 @@ use App\Entity\Core\AbstractBaseAttribute; use App\Entity\Core\Attribute; -final readonly class AttributeAssigner +class AttributeAssigner { public function __construct(private AttributeTypeRegistry $attributeTypeRegistry) { diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php new file mode 100644 index 000000000..9d72881c8 --- /dev/null +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -0,0 +1,144 @@ + [ + 'definition' => [ + 'name' => 'Title', + 'isMultiple' => false, + 'initialValues' => [ + '_' => '{ "type": "metadata", "value": "XMP-dc:Title"}', + 'en' => '{ "type": "metadata", "value": "description"}', + ], + 'fieldType' => TextAttributeType::NAME, + ], + 'metadata' => [ + 'Composite:GPSPosition' => '48.8588443, 2.2943506', + 'XMP-dc:Title' => 'Test Title', + 'description' => 'Test Description', + ], + 'expected' => [ + 'Title' => [ + '_' => ['Test Title'], + 'en' => ['Test Description'], + ], + ], + ], + 'test2' => [ + 'definition' => [ + 'name' => 'Keywords', + 'isMultiple' => true, + 'initialValues' => [ + '_' => '{ "type": "metadata", "value": "IPTC:Keywords"}', + ], + 'fieldType' => TextAttributeType::NAME, + ], + 'metadata' => [ + 'IPTC:Keywords' => ['dog', 'cat', 'bird'], + ], + 'expected' => [ + 'Keywords' => [ + '_' => ['cat', 'dog', 'bird'], + ], + ], + ], + ]; + } + + /** + * @dataProvider dataProvider + * + * @param array $definition + * @param array $metadata + */ + public function testResolveInitialAttributes(array $definition, array $metadata, array $expected): void + { + self::bootKernel(); + $container = static::getContainer(); + + $atr = $this->createMock(AttributeDefinition::class); + $atr->expects($this->any())->method('getName') + ->willReturn($definition['name']); + $atr->expects($this->any())->method('isMultiple') + ->willReturn($definition['isMultiple']); + $atr->expects($this->any())->method('getInitialValues') + ->willReturn($definition['initialValues']); + $atr->expects($this->any())->method('getFieldType') + ->willReturn($definition['fieldType']); + + /** @var AttributeDefinitionRepository $adr */ + $adr = $this->createMock(AttributeDefinitionRepository::class); + $adr->expects($this->any()) + ->method('getWorkspaceInitializeDefinitions') + ->willReturn([$atr]); + + /** @var File $fileMock */ + $fileMock = $this->createMock(File::class); + + $data = is_array($metadata) ? $metadata : [$metadata]; + $fileMock->expects($this->any()) + ->method('getMetadata') + ->willReturn($this->toMetadata($metadata)); + + /** @var Asset $assetMock */ + $assetMock = $this->createMock(Asset::class); + $assetMock->expects($this->any()) + ->method('getSource') + ->willReturn($fileMock); + + // ================================================ + + $iavr = new InitialAttributeValuesResolver( + $adr, + $container->get(AttributeAssigner::class) + ); + + $result = []; + /** @var Attribute $attribute */ + foreach ($iavr->resolveInitialAttributes($assetMock) as $attribute) { + $result[$attribute->getDefinition()->getName()] ??= []; + $result[$attribute->getDefinition()->getName()][$attribute->getLocale()] ??= []; + $result[$attribute->getDefinition()->getName()][$attribute->getLocale()][] = $attribute->getValue(); + } + + $this->assertEqualsCanonicalizing($expected, $result); + } + + private function toMetadata($data) + { + $metadata = []; + foreach ($data as $key => $value) { + if (is_array($value)) { + $metadata[$key] = [ + 'value' => join(' ; ', $value), + 'values' => $value, + ]; + } else { + $metadata[$key] = [ + 'value' => $value, + 'values' => [$value], + ]; + } + } + + return $metadata; + } +} From c28b48b9122d6984e720b1719552a7aee4499533 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Fri, 27 Jun 2025 11:32:57 +0200 Subject: [PATCH 15/28] wip --- .../InitialAttributeValuesResolverTest.php | 166 ++++++++++-------- .../InitialAttributeValuesResolverData.yaml | 94 ++++++++++ 2 files changed, 187 insertions(+), 73 deletions(-) create mode 100644 databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 9d72881c8..018ad9fea 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -14,101 +14,80 @@ use App\Repository\Core\AttributeDefinitionRepository; use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\Yaml\Yaml; class InitialAttributeValuesResolverTest extends KernelTestCase { + private AttributeAssigner $attributeAssigner; + + public function setUp(): void + { + self::bootKernel(); + $this->attributeAssigner = static::getContainer()->get(AttributeAssigner::class); + } + public static function dataProvider(): array { - return [ - 'test1' => [ - 'definition' => [ - 'name' => 'Title', - 'isMultiple' => false, - 'initialValues' => [ - '_' => '{ "type": "metadata", "value": "XMP-dc:Title"}', - 'en' => '{ "type": "metadata", "value": "description"}', - ], - 'fieldType' => TextAttributeType::NAME, - ], - 'metadata' => [ - 'Composite:GPSPosition' => '48.8588443, 2.2943506', - 'XMP-dc:Title' => 'Test Title', - 'description' => 'Test Description', - ], - 'expected' => [ - 'Title' => [ - '_' => ['Test Title'], - 'en' => ['Test Description'], - ], - ], - ], - 'test2' => [ - 'definition' => [ - 'name' => 'Keywords', - 'isMultiple' => true, - 'initialValues' => [ - '_' => '{ "type": "metadata", "value": "IPTC:Keywords"}', - ], - 'fieldType' => TextAttributeType::NAME, - ], - 'metadata' => [ - 'IPTC:Keywords' => ['dog', 'cat', 'bird'], - ], - 'expected' => [ - 'Keywords' => [ - '_' => ['cat', 'dog', 'bird'], - ], - ], - ], - ]; + return array_map( + function ($test) { + return [ + $test['definitions'], + $test['metadata'], + $test['expected'], + ]; + }, + array_filter( + Yaml::parseFile(__DIR__.'/../../fixtures/metadata/InitialAttributeValuesResolverData.yaml'), + function ($test) { + return $test['enabled'] ?? true; + } + ) + ); } /** * @dataProvider dataProvider * - * @param array $definition - * @param array $metadata + * @param array $metadata */ - public function testResolveInitialAttributes(array $definition, array $metadata, array $expected): void + public function testResolveInitialAttributes(array $definitions, array $metadata, array $expected): void { - self::bootKernel(); - $container = static::getContainer(); - - $atr = $this->createMock(AttributeDefinition::class); - $atr->expects($this->any())->method('getName') - ->willReturn($definition['name']); - $atr->expects($this->any())->method('isMultiple') - ->willReturn($definition['isMultiple']); - $atr->expects($this->any())->method('getInitialValues') - ->willReturn($definition['initialValues']); - $atr->expects($this->any())->method('getFieldType') - ->willReturn($definition['fieldType']); - - /** @var AttributeDefinitionRepository $adr */ + $attributeDefinitions = []; + foreach ($definitions as $name => $definition) { + if (null !== ($initialValues = $definition['initialValues'] ?? null)) { + $initialValues = is_array($initialValues) ? $initialValues : ['_' => $initialValues]; + } + $ad = $this->createMock(AttributeDefinition::class); + $ad->expects($this->any())->method('getName') + ->willReturn($name); + $ad->expects($this->any())->method('isMultiple') + ->willReturn($definition['isMultiple'] ?? false); + $ad->expects($this->any())->method('getInitialValues') + ->willReturn($initialValues); + $ad->expects($this->any())->method('getFieldType') + ->willReturn($definition['fieldType'] ?? TextAttributeType::NAME); + $attributeDefinitions[] = $ad; + } + $adr = $this->createMock(AttributeDefinitionRepository::class); $adr->expects($this->any()) ->method('getWorkspaceInitializeDefinitions') - ->willReturn([$atr]); + ->willReturn($attributeDefinitions); - /** @var File $fileMock */ $fileMock = $this->createMock(File::class); - $data = is_array($metadata) ? $metadata : [$metadata]; $fileMock->expects($this->any()) ->method('getMetadata') - ->willReturn($this->toMetadata($metadata)); + ->willReturn($this->conformMetadata($metadata)); - /** @var Asset $assetMock */ $assetMock = $this->createMock(Asset::class); $assetMock->expects($this->any()) ->method('getSource') ->willReturn($fileMock); - // ================================================ - $iavr = new InitialAttributeValuesResolver( $adr, - $container->get(AttributeAssigner::class) + $this->attributeAssigner ); $result = []; @@ -119,26 +98,67 @@ public function testResolveInitialAttributes(array $definition, array $metadata, $result[$attribute->getDefinition()->getName()][$attribute->getLocale()][] = $attribute->getValue(); } - $this->assertEqualsCanonicalizing($expected, $result); + $this->assertEquals($this->conformExpected($expected), $result); + } + + private function conformExpected(array $expected): array + { + $conformed = []; + foreach ($expected as $attributeName => $value) { + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple list of values + $conformed[$attributeName] = ['_' => $value]; + } else { + // an array with key=locale + $conformed[$attributeName] = array_map( + function ($v) { + return is_array($v) ? $v : [$v]; + }, + $value + ); + } + } else { + // a single value + $conformed[$attributeName] = ['_' => [$value]]; + } + } + + return $conformed; + } + + private function isNumericArray($a): bool + { + if (!is_array($a)) { + return false; + } + foreach ($a as $k => $v) { + if (!is_numeric($k)) { + return false; + } + } + + return true; } - private function toMetadata($data) + private function conformMetadata($data): array { - $metadata = []; + $conformed = []; + $data = is_array($data) ? $data : [$data]; foreach ($data as $key => $value) { if (is_array($value)) { - $metadata[$key] = [ + $conformed[$key] = [ 'value' => join(' ; ', $value), 'values' => $value, ]; } else { - $metadata[$key] = [ + $conformed[$key] = [ 'value' => $value, 'values' => [$value], ]; } } - return $metadata; + return $conformed; } } diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml new file mode 100644 index 000000000..d0e94698e --- /dev/null +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -0,0 +1,94 @@ +# shortcuts +# +# - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. +# if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) +# - same principle applies for expected/ values (no locale == '_' locale) +# +# - expected values is an array, possibly with only one element +# if only one value is expected, the plain value can be used (it will test with [value]) +# +# - fieldType defaults to text +# +# - isMultiple defaults to false +# +# - possible fieldTypes are: +# boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text + +exiftoolVersion: + about: + title: 'Vérification du fonctionnement de l''intégration metadata-read' + description: + 'Cette lecture permet de s''assurer du bon déroulement du processus d''extraction des métadonnées. + La metadata _ExifTool:ExifToolVersion_ est toujours retournée par exiftool.' + definitions: + exiftoolVersion: + initialValues: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' + metadata: + 'ExifTool:ExifToolVersion': '12.42' + expected: + exiftoolVersion: '12.42' + +multi2multi: + about: + title: 'Meta multi-valuée -> attribut multi-valué (méthode "metadata")' + definitions: + keywords: + isMultiple: true + initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' + metadata: + 'IPTC:Keywords': ['keyword1', 'keyword2', 'keyword3'] + expected: + keywords: ['keyword1', 'keyword2', 'keyword3'] + +exiftoolVersion2: + enabled: false + definitions: + exiftoolVersion: + fieldType: text + isMultiple: false + initialValues: + _: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' + metadata: + 'ExifTool:ExifToolVersion': '12.42' + expected: + exiftoolVersion: + _: + - '12.42' +test1: + enabled: false + definitions: + Title: + isMultiple: false + initialValues: + _: '{ "type": "metadata", "value": "XMP-dc:Title"}' + en: '{ "type": "metadata", "value": "description"}' + fieldType: text + metadata: + 'Composite:GPSPosition': '48.8588443, 2.2943506' + 'XMP-dc:Title': Test Title + description: Test Description + expected: + Title: + _: + - Test Title + en: + - Test Description +test2: + enabled: false + definitions: + Keywords: + isMultiple: true + initialValues: + _: '{ "type": "metadata", "value": "IPTC:Keywords"}' + fieldType: text + metadata: + 'IPTC:Keywords': + - dog + - cat + - bird + expected: + Keywords: + _: + - cat + - dog + - bird From b3067d0b3c1231ed3fe2c54eea1a4b37c72eefa7 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 2 Jul 2025 11:22:00 +0200 Subject: [PATCH 16/28] wip --- .../Command/DocumentationDumperCommand.php | 114 +++++++++++++++++- .../InitialAttributeValuesResolverData.yaml | 104 +++++++++------- 2 files changed, 174 insertions(+), 44 deletions(-) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index f927639d6..3cd3c7b02 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Yaml\Yaml; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command @@ -21,9 +22,118 @@ public function __construct( protected function execute(InputInterface $input, OutputInterface $output): int { - $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); - $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + // $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); + // $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + + $this->dumpInitialValuesDocumentation($output); return Command::SUCCESS; } + + private function dumpInitialValuesDocumentation(OutputInterface $output) + { + $n = 0; + foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { + if (!($test['about'] ?? false)) { + continue; + } + + if ($n++ > 0) { + $output->writeln(''); + $output->writeln('---'); + $output->writeln(''); + } + + $output->writeln(sprintf('## %s', $test['about']['title'] ?? '')); + if ($description = $test['about']['description'] ?? '') { + $output->writeln(sprintf('%s', $description)); + } + + $output->writeln('### Attribute(s) definition(s) ...'); + foreach ($test['definitions'] as $name => $definition) { + $output->write(sprintf('- __%s__:', $name)); + $output->write(sprintf(' `%s`', $definition['fieldType'] ?? 'text')); + $output->write($definition['isMultiple'] ?? false ? sprintf(' ; [x] `multi`') : ''); + + $output->writeln(''); + + if (is_array($definition['initialValues'] ?? null)) { + foreach ($definition['initialValues'] as $locale => $initializer) { + $output->writeln(sprintf(' - locale `%s`', $locale)); + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 1); + } + } elseif (null !== ($definition['initialValues'] ?? null)) { + $initializer = $definition['initialValues']; + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 0); + } + + $nCols = 0; + foreach ($test['metadata'] as $values) { + $nCols = max($nCols, is_array($values) ? count($values) : 1); + } + foreach ($test['metadata'] as $metadataName => $values) { + $v = is_array($values) ? $values : [$values]; + $output->writeln(sprintf('| %s | %s |', $metadataName, join(' | ', $v))); + } + + $output->writeln('### ... initial attribute(s) value(s)'); + foreach ($test['expected'] as $attributeName => $value) { + // $output->writeln(sprintf('- __%s__:', $attributeName)); + $this->dumpExpected($output, $attributeName, $value, 1); + } + } + + } + } + + private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 1): void + { + $tab = str_repeat(' ', $indent); + $output->writeln(sprintf('%s```%s', $tab, $language)); + foreach (explode("\n", $code) as $line) { + $output->writeln(sprintf('%s%s', $tab, $line)); + } + $output->writeln(sprintf('%s```', $tab)); + } + + private function dumpExpected(OutputInterface $output, string $attributeName, $value, int $indent = 0): void + { + $tab = str_repeat(' ', $indent); + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple list of values + $n = 1; + foreach ($value as $v) { + $output->writeln(sprintf('%s%s #%d: `%s`', $tab, $attributeName, $n++, $v)); + $output->writeln(''); + } + } else { + // an array with key=locale + foreach ($value as $locale => $v) { + $output->writeln(sprintf('%s- locale `%s`:', $tab, $locale)); + $output->writeln(''); + $this->dumpExpected($output, $attributeName, $v, $indent + 1); + } + } + } else { + // a single value + $output->writeln(sprintf('%s`%s: %s`', $tab, $attributeName, $value)); + } + } + + private function isNumericArray($a): bool + { + if (!is_array($a)) { + return false; + } + foreach ($a as $k => $v) { + if (!is_numeric($k)) { + return false; + } + } + + return true; + } } diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index d0e94698e..7c27db4c8 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -19,7 +19,7 @@ exiftoolVersion: title: 'Vérification du fonctionnement de l''intégration metadata-read' description: 'Cette lecture permet de s''assurer du bon déroulement du processus d''extraction des métadonnées. - La metadata _ExifTool:ExifToolVersion_ est toujours retournée par exiftool.' + La metadata `ExifTool:ExifToolVersion` est toujours retournée par exiftool.' definitions: exiftoolVersion: initialValues: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' @@ -28,67 +28,87 @@ exiftoolVersion: expected: exiftoolVersion: '12.42' +multi2mono: + about: + title: 'Meta multi-valuée -> attribut mono-valué (méthode "metadata")' + description: + 'Les valeurs seront séparées par des points-virgules.' + definitions: + Keywords: + isMultiple: false + initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' + metadata: + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + expected: + Keywords: 'chien ; chat ; oiseau' + multi2multi: about: title: 'Meta multi-valuée -> attribut multi-valué (méthode "metadata")' definitions: - keywords: + Keywords: isMultiple: true initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' metadata: - 'IPTC:Keywords': ['keyword1', 'keyword2', 'keyword3'] + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: - keywords: ['keyword1', 'keyword2', 'keyword3'] + Keywords: ['chien', 'chat', 'oiseau'] -exiftoolVersion2: - enabled: false +multi2mono_template: + about: + title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' + description: 'attention : utilisation erronée de getMetadata(...).value (sans "s")' definitions: - exiftoolVersion: - fieldType: text - isMultiple: false - initialValues: - _: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' + Keywords: + isMultiple: true + initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' metadata: - 'ExifTool:ExifToolVersion': '12.42' + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: - exiftoolVersion: - _: - - '12.42' -test1: - enabled: false + Keywords: 'chien ; chat ; oiseau' + +multi2multi_template: + about: + title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' + description: 'Utilisation correcte de getMetadata(...).value__s__' definitions: - Title: - isMultiple: false + Keywords: + isMultiple: true + initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values }}"}' + metadata: + 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + expected: + Keywords: ['chien', 'chat', 'oiseau'] + +mono_multilingue: + about: + title: 'mono multilingue' + definitions: + Copyright: initialValues: - _: '{ "type": "metadata", "value": "XMP-dc:Title"}' - en: '{ "type": "metadata", "value": "description"}' - fieldType: text + en: '{ "type": "template", "value": "(c) {{ file.getMetadata(''XMP-dc:Creator'').value }}. All rights reserved" }' + fr: '{ "type": "template", "value": "(c) {{ file.getMetadata(''XMP-dc:Creator'').value }}. Tous droits réservés" }' metadata: - 'Composite:GPSPosition': '48.8588443, 2.2943506' - 'XMP-dc:Title': Test Title - description: Test Description + 'XMP-dc:Creator': 'Bob' expected: - Title: - _: - - Test Title - en: - - Test Description -test2: - enabled: false + Copyright: + en: '(c) Bob. All rights reserved' + fr: '(c) Bob. Tous droits réservés' + +multi_multilingue: + about: + title: 'multi multilingue' definitions: Keywords: isMultiple: true initialValues: - _: '{ "type": "metadata", "value": "IPTC:Keywords"}' - fieldType: text + en: '{ "type": "metadata", "value": "XMP-dc:Subject" }' + fr: '{ "type": "metadata", "value": "IPTC:SupplementalCategories" }' metadata: - 'IPTC:Keywords': - - dog - - cat - - bird + 'XMP-dc:Subject': ['dog', 'cat', 'bird'] + 'IPTC:SupplementalCategories': ['chien', 'chat', 'oiseau'] expected: Keywords: - _: - - cat - - dog - - bird + en: ['dog', 'cat', 'bird'] + fr: ['chien', 'chat', 'oiseau'] + From 759807ac1b902fe687b716c3a12fdd5135561c95 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 2 Jul 2025 20:33:51 +0200 Subject: [PATCH 17/28] wip --- .../Command/DocumentationDumperCommand.php | 105 ++++++++++++------ .../InitialAttributeValuesResolverTest.php | 2 + 2 files changed, 75 insertions(+), 32 deletions(-) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 3cd3c7b02..4eee82ca8 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -7,6 +7,7 @@ use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Yaml; @@ -20,18 +21,46 @@ public function __construct( parent::__construct(); } + protected function configure(): void + { + parent::configure(); + + $this + ->setDescription('Dump code-generated documentation(s)') + ->addArgument('part', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Part(s) to dump. If not specified, all parts will be dumped.') + ->setHelp('parts: "rendition", "initial-attribute"') + ; + } + protected function execute(InputInterface $input, OutputInterface $output): int { - // $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); - // $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + if (empty($input->getArgument('part'))) { + $input->setArgument('part', ['rendition', 'initial-attribute']); + } - $this->dumpInitialValuesDocumentation($output); + foreach ($input->getArgument('part') as $part) { + if (!in_array($part, ['rendition', 'initial-attribute'])) { + $output->writeln(sprintf('Unknown part "%s". Valid parts are "rendition" and "initial-attribute".', $part)); + + return Command::FAILURE; + } + switch ($part) { + case 'rendition': + $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); + $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); + break; + case 'initial-attribute': + $this->dumpInitialValuesDocumentation($output); + break; + } + } return Command::SUCCESS; } private function dumpInitialValuesDocumentation(OutputInterface $output) { + $output->writeln('# Initial Attribute Values'); $n = 0; foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { if (!($test['about'] ?? false)) { @@ -53,7 +82,8 @@ private function dumpInitialValuesDocumentation(OutputInterface $output) foreach ($test['definitions'] as $name => $definition) { $output->write(sprintf('- __%s__:', $name)); $output->write(sprintf(' `%s`', $definition['fieldType'] ?? 'text')); - $output->write($definition['isMultiple'] ?? false ? sprintf(' ; [x] `multi`') : ''); + $output->write(sprintf(' ; [%s] `multi`', $definition['isMultiple'] ?? false ? 'X' : ' ')); + $output->write(sprintf(' ; [%s] `translatable`', $definition['isTranslatable'] ?? false ? 'X' : ' ')); $output->writeln(''); @@ -69,26 +99,26 @@ private function dumpInitialValuesDocumentation(OutputInterface $output) $this->codeBlockIndented($output, $code, 'json', 0); } - $nCols = 0; - foreach ($test['metadata'] as $values) { - $nCols = max($nCols, is_array($values) ? count($values) : 1); - } + $output->writeln(''); + $output->writeln('### ... with file metadata ...'); + $output->writeln('| metadata | value(s) |'); + $output->writeln('|---|---|'); foreach ($test['metadata'] as $metadataName => $values) { $v = is_array($values) ? $values : [$values]; - $output->writeln(sprintf('| %s | %s |', $metadataName, join(' | ', $v))); + $output->writeln(sprintf('| %s | `%s` |', $metadataName, join('` ; `', $v))); } - $output->writeln('### ... initial attribute(s) value(s)'); - foreach ($test['expected'] as $attributeName => $value) { - // $output->writeln(sprintf('- __%s__:', $attributeName)); - $this->dumpExpected($output, $attributeName, $value, 1); - } + $output->writeln(''); + $output->writeln('### ... set attribute(s) initial value(s)'); + $this->dumpExpected($output, $test['expected'], 1); + + $output->writeln(''); } } } - private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 1): void + private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 0): void { $tab = str_repeat(' ', $indent); $output->writeln(sprintf('%s```%s', $tab, $language)); @@ -98,28 +128,39 @@ private function codeBlockIndented(OutputInterface $output, string $code, string $output->writeln(sprintf('%s```', $tab)); } - private function dumpExpected(OutputInterface $output, string $attributeName, $value, int $indent = 0): void + private function dumpExpected(OutputInterface $output, array $expected, int $indent = 0): void { $tab = str_repeat(' ', $indent); - if (is_array($value)) { - if ($this->isNumericArray($value)) { - // a simple list of values - $n = 1; - foreach ($value as $v) { - $output->writeln(sprintf('%s%s #%d: `%s`', $tab, $attributeName, $n++, $v)); - $output->writeln(''); + $output->writeln(sprintf('%s| Attributes | initial value(s) |', $tab)); + $output->writeln(sprintf('%s|---|---|', $tab)); + foreach ($expected as $attributeName => $value) { + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple array of values + $n = 1; + foreach ($value as $v) { + $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n++, $v)); + // $output->writeln(''); + } + } else { + // an array with key=locale + foreach ($value as $locale => $v) { + $output->writeln(sprintf('%s| __locale `%s`__ | |', $tab, $locale)); + if (is_array($v)) { + // an array of values + foreach ($v as $n => $w) { + $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n + 1, $w)); + } + } else { + $output->writeln(sprintf('%s| %s | `%s` |', $tab, $attributeName, $v)); + } + // $output->writeln(''); + } } } else { - // an array with key=locale - foreach ($value as $locale => $v) { - $output->writeln(sprintf('%s- locale `%s`:', $tab, $locale)); - $output->writeln(''); - $this->dumpExpected($output, $attributeName, $v, $indent + 1); - } + // a single value + $output->writeln(sprintf('%s| %s | `%s`', $tab, $attributeName, $value)); } - } else { - // a single value - $output->writeln(sprintf('%s`%s: %s`', $tab, $attributeName, $value)); } } diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 018ad9fea..c78b115fd 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -62,6 +62,8 @@ public function testResolveInitialAttributes(array $definitions, array $metadata ->willReturn($name); $ad->expects($this->any())->method('isMultiple') ->willReturn($definition['isMultiple'] ?? false); + $ad->expects($this->any())->method('isTranslatable') + ->willReturn($definition['isTranslatable'] ?? false); $ad->expects($this->any())->method('getInitialValues') ->willReturn($initialValues); $ad->expects($this->any())->method('getFieldType') From 8b88685eb3c2aeae93de8c15bd88a54896eac550 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Wed, 2 Jul 2025 20:47:46 +0200 Subject: [PATCH 18/28] fix --- .../fixtures/metadata/InitialAttributeValuesResolverData.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index 7c27db4c8..43230d2c3 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -61,7 +61,7 @@ multi2mono_template: definitions: Keywords: isMultiple: true - initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' + initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' metadata: 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: @@ -74,7 +74,7 @@ multi2multi_template: definitions: Keywords: isMultiple: true - initialValues: '{"method": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values }}"}' + initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values | join(''\n'') }}"}' metadata: 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] expected: From 1223044cb45fcb5bad48b64a110d1509cc36bb43 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 3 Jul 2025 18:13:04 +0200 Subject: [PATCH 19/28] auto-find documentation generators ; output to stdout or file --- .../Command/DocumentationDumperCommand.php | 178 +++++------------- .../DocumentationGeneratorInterface.php | 17 ++ .../InitialValuesDocumentationGenerator.php | 136 +++++++++++++ ...RenditionBuilderDocumentationGenerator.php | 22 +++ 4 files changed, 218 insertions(+), 135 deletions(-) create mode 100644 databox/api/src/documentation/DocumentationGeneratorInterface.php create mode 100644 databox/api/src/documentation/InitialValuesDocumentationGenerator.php create mode 100644 databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 4eee82ca8..8c86ddaee 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -4,19 +4,28 @@ namespace App\Command; -use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; +use App\documentation\DocumentationGeneratorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Yaml\Yaml; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; +use Symfony\Component\String\Slugger\AsciiSlugger; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command { + /** + * @uses InitialValuesDocumentationGenerator + * @uses RenditionBuilderDocumentationGenerator + */ + + /** @var array */ + private array $chapters = []; + public function __construct( - private readonly RenditionBuilderConfigurationDocumentation $renditionBuilderConfigurationDocumentation, + #[AutowireIterator(DocumentationGeneratorInterface::TAG)] private readonly iterable $documentations, ) { parent::__construct(); } @@ -25,156 +34,55 @@ protected function configure(): void { parent::configure(); + $slugger = new AsciiSlugger(); + /** @var DocumentationGeneratorInterface $documentation */ + foreach ($this->documentations as $documentation) { + $name = strtolower($slugger->slug($documentation::getName())->toString()); + if (isset($this->chapters[$documentation::getName()])) { + throw new \LogicException(sprintf('Chapter "%s" is already registered.', $name)); + } + $this->chapters[$name] = $documentation; + } + $this ->setDescription('Dump code-generated documentation(s)') - ->addArgument('part', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Part(s) to dump. If not specified, all parts will be dumped.') - ->setHelp('parts: "rendition", "initial-attribute"') + ->addArgument('chapters', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Chapter(s) to dump. If not specified, all chapters will be dumped.') + ->addOption('output', 'o', InputArgument::OPTIONAL, 'Output directory to write the documentation to. If not specified, it will be written to stdout.') + ->setHelp(sprintf('chapters: "%s"', join('", "', array_keys($this->chapters)))) ; } protected function execute(InputInterface $input, OutputInterface $output): int { - if (empty($input->getArgument('part'))) { - $input->setArgument('part', ['rendition', 'initial-attribute']); - } - - foreach ($input->getArgument('part') as $part) { - if (!in_array($part, ['rendition', 'initial-attribute'])) { - $output->writeln(sprintf('Unknown part "%s". Valid parts are "rendition" and "initial-attribute".', $part)); + $outputDir = $input->getOption('output'); + if ($outputDir && !is_dir($outputDir)) { + $output->writeln(sprintf('Output directory "%s" does not exists.', $outputDir)); - return Command::FAILURE; - } - switch ($part) { - case 'rendition': - $output->writeln('# '.$this->renditionBuilderConfigurationDocumentation::getName()); - $output->writeln($this->renditionBuilderConfigurationDocumentation->generate()); - break; - case 'initial-attribute': - $this->dumpInitialValuesDocumentation($output); - break; - } + return Command::FAILURE; } - return Command::SUCCESS; - } - - private function dumpInitialValuesDocumentation(OutputInterface $output) - { - $output->writeln('# Initial Attribute Values'); - $n = 0; - foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { - if (!($test['about'] ?? false)) { - continue; - } - - if ($n++ > 0) { - $output->writeln(''); - $output->writeln('---'); - $output->writeln(''); - } - - $output->writeln(sprintf('## %s', $test['about']['title'] ?? '')); - if ($description = $test['about']['description'] ?? '') { - $output->writeln(sprintf('%s', $description)); - } - - $output->writeln('### Attribute(s) definition(s) ...'); - foreach ($test['definitions'] as $name => $definition) { - $output->write(sprintf('- __%s__:', $name)); - $output->write(sprintf(' `%s`', $definition['fieldType'] ?? 'text')); - $output->write(sprintf(' ; [%s] `multi`', $definition['isMultiple'] ?? false ? 'X' : ' ')); - $output->write(sprintf(' ; [%s] `translatable`', $definition['isTranslatable'] ?? false ? 'X' : ' ')); - - $output->writeln(''); - - if (is_array($definition['initialValues'] ?? null)) { - foreach ($definition['initialValues'] as $locale => $initializer) { - $output->writeln(sprintf(' - locale `%s`', $locale)); - $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - $this->codeBlockIndented($output, $code, 'json', 1); - } - } elseif (null !== ($definition['initialValues'] ?? null)) { - $initializer = $definition['initialValues']; - $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - $this->codeBlockIndented($output, $code, 'json', 0); - } - - $output->writeln(''); - $output->writeln('### ... with file metadata ...'); - $output->writeln('| metadata | value(s) |'); - $output->writeln('|---|---|'); - foreach ($test['metadata'] as $metadataName => $values) { - $v = is_array($values) ? $values : [$values]; - $output->writeln(sprintf('| %s | `%s` |', $metadataName, join('` ; `', $v))); - } - - $output->writeln(''); - $output->writeln('### ... set attribute(s) initial value(s)'); - $this->dumpExpected($output, $test['expected'], 1); + foreach ($input->getArgument('chapters') as $chapter) { + if (!isset($this->chapters[$chapter])) { + $output->writeln(sprintf('Unknown chapter "%s". Available chapters are "%s"', $chapter, join('", "', array_keys($this->chapters)))); - $output->writeln(''); + return Command::FAILURE; } - } - } - private function codeBlockIndented(OutputInterface $output, string $code, string $language, int $indent = 0): void - { - $tab = str_repeat(' ', $indent); - $output->writeln(sprintf('%s```%s', $tab, $language)); - foreach (explode("\n", $code) as $line) { - $output->writeln(sprintf('%s%s', $tab, $line)); + if (empty($input->getArgument('chapters'))) { + $input->setArgument('chapters', array_keys($this->chapters)); } - $output->writeln(sprintf('%s```', $tab)); - } - - private function dumpExpected(OutputInterface $output, array $expected, int $indent = 0): void - { - $tab = str_repeat(' ', $indent); - $output->writeln(sprintf('%s| Attributes | initial value(s) |', $tab)); - $output->writeln(sprintf('%s|---|---|', $tab)); - foreach ($expected as $attributeName => $value) { - if (is_array($value)) { - if ($this->isNumericArray($value)) { - // a simple array of values - $n = 1; - foreach ($value as $v) { - $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n++, $v)); - // $output->writeln(''); - } - } else { - // an array with key=locale - foreach ($value as $locale => $v) { - $output->writeln(sprintf('%s| __locale `%s`__ | |', $tab, $locale)); - if (is_array($v)) { - // an array of values - foreach ($v as $n => $w) { - $output->writeln(sprintf('%s| %s #%d | `%s` |', $tab, $attributeName, $n + 1, $w)); - } - } else { - $output->writeln(sprintf('%s| %s | `%s` |', $tab, $attributeName, $v)); - } - // $output->writeln(''); - } - } + foreach ($input->getArgument('chapters') as $chapter) { + $text = '# '.$this->chapters[$chapter]->getName()."\n".$this->chapters[$chapter]->generate(); + if ($outputDir) { + $outputFile = rtrim($outputDir, '/').'/'.$chapter.'.md'; + file_put_contents($outputFile, $text); + $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); } else { - // a single value - $output->writeln(sprintf('%s| %s | `%s`', $tab, $attributeName, $value)); + $output->writeln($text); } } - } - private function isNumericArray($a): bool - { - if (!is_array($a)) { - return false; - } - foreach ($a as $k => $v) { - if (!is_numeric($k)) { - return false; - } - } - - return true; + return Command::SUCCESS; } } diff --git a/databox/api/src/documentation/DocumentationGeneratorInterface.php b/databox/api/src/documentation/DocumentationGeneratorInterface.php new file mode 100644 index 000000000..8e2126c4b --- /dev/null +++ b/databox/api/src/documentation/DocumentationGeneratorInterface.php @@ -0,0 +1,17 @@ + 0) { + $output .= "\n---\n"; + } + + $output .= sprintf("## %s\n", $test['about']['title'] ?? ''); + if ($description = $test['about']['description'] ?? '') { + $output .= sprintf("%s\n", $description); + } + + $output .= "### Attribute(s) definition(s) ...\n"; + foreach ($test['definitions'] as $name => $definition) { + $output .= sprintf("- __%s__: `%s` ; [%s] `multi` ; [%s] `translatable`\n", + $name, + $definition['fieldType'] ?? 'text', + $definition['isMultiple'] ?? false ? 'X' : ' ', + $definition['isTranslatable'] ?? false ? 'X' : ' ' + ); + + $output .= "\n"; + + if (is_array($definition['initialValues'] ?? null)) { + foreach ($definition['initialValues'] as $locale => $initializer) { + $output .= sprintf(" - locale `%s`\n", $locale); + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 1); + } + } elseif (null !== ($definition['initialValues'] ?? null)) { + $initializer = $definition['initialValues']; + $code = json_encode(json_decode($initializer), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $this->codeBlockIndented($output, $code, 'json', 0); + } + + $output .= "\n"; + + $output .= "### ... with file metadata ...\n"; + $output .= "| metadata | value(s) |\n"; + $output .= "|---|---|\n"; + foreach ($test['metadata'] as $metadataName => $values) { + $v = is_array($values) ? $values : [$values]; + $output .= sprintf("| %s | `%s` |\n", $metadataName, join('` ; `', $v)); + } + + $output .= "\n"; + + $output .= "### ... set attribute(s) initial value(s)\n"; + $this->dumpExpected($output, $test['expected'], 1); + + $output .= "\n"; + } + } + + return $output; + } + + private function codeBlockIndented(string &$output, string $code, string $language, int $indent = 0): void + { + $tab = str_repeat(' ', $indent); + $output .= sprintf("%s```%s\n", $tab, $language); + foreach (explode("\n", $code) as $line) { + $output .= sprintf("%s%s\n", $tab, $line); + } + $output .= sprintf("%s```\n", $tab); + } + + private function dumpExpected(string &$output, array $expected, int $indent = 0): void + { + $tab = str_repeat(' ', $indent); + $output .= sprintf("%s| Attributes | initial value(s) |\n", $tab); + $output .= sprintf("%s|---|---|\n", $tab); + foreach ($expected as $attributeName => $value) { + if (is_array($value)) { + if ($this->isNumericArray($value)) { + // a simple array of values + $n = 1; + foreach ($value as $v) { + $output .= sprintf("%s| %s #%d | `%s` |\n", $tab, $attributeName, $n++, $v); + } + } else { + // an array with key=locale + foreach ($value as $locale => $v) { + $output .= sprintf("%s| __locale `%s`__ | |\n", $tab, $locale); + if (is_array($v)) { + // an array of values + foreach ($v as $n => $w) { + $output .= sprintf("%s| %s #%d | `%s` |\n", $tab, $attributeName, $n + 1, $w); + } + } else { + $output .= sprintf("%s| %s | `%s` |\n", $tab, $attributeName, $v); + } + } + } + } else { + // a single value + $output .= sprintf("%s| %s | `%s`\n", $tab, $attributeName, $value); + } + } + } + + private function isNumericArray($a): bool + { + if (!is_array($a)) { + return false; + } + foreach ($a as $k => $v) { + if (!is_numeric($k)) { + return false; + } + } + + return true; + } +} diff --git a/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php new file mode 100644 index 000000000..17c83aebc --- /dev/null +++ b/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php @@ -0,0 +1,22 @@ +renditionBuilderConfigurationDocumentation->generate(); + } +} From bb40530df604724e4eb088c6635756173a5eec89 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 3 Jul 2025 18:32:46 +0200 Subject: [PATCH 20/28] cs --- databox/api/src/Command/DocumentationDumperCommand.php | 2 +- .../DocumentationGeneratorInterface.php | 4 ++-- .../InitialValuesDocumentationGenerator.php | 2 +- .../RenditionBuilderDocumentationGenerator.php | 4 +++- 4 files changed, 7 insertions(+), 5 deletions(-) rename databox/api/src/{documentation => Documentation}/DocumentationGeneratorInterface.php (74%) rename databox/api/src/{documentation => Documentation}/InitialValuesDocumentationGenerator.php (99%) rename databox/api/src/{documentation => Documentation}/RenditionBuilderDocumentationGenerator.php (90%) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 8c86ddaee..a27e7d9c1 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -4,7 +4,7 @@ namespace App\Command; -use App\documentation\DocumentationGeneratorInterface; +use App\Documentation\DocumentationGeneratorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; diff --git a/databox/api/src/documentation/DocumentationGeneratorInterface.php b/databox/api/src/Documentation/DocumentationGeneratorInterface.php similarity index 74% rename from databox/api/src/documentation/DocumentationGeneratorInterface.php rename to databox/api/src/Documentation/DocumentationGeneratorInterface.php index 8e2126c4b..06b0f1b2d 100644 --- a/databox/api/src/documentation/DocumentationGeneratorInterface.php +++ b/databox/api/src/Documentation/DocumentationGeneratorInterface.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace App\documentation; +namespace App\Documentation; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; #[AutoconfigureTag(self::TAG)] interface DocumentationGeneratorInterface { - final public const TAG = 'documentation_generator'; + final public const string TAG = 'documentation_generator'; public static function getName(): string; diff --git a/databox/api/src/documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php similarity index 99% rename from databox/api/src/documentation/InitialValuesDocumentationGenerator.php rename to databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 3cfa4b076..0d6be4e7e 100644 --- a/databox/api/src/documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\documentation; +namespace App\Documentation; use Symfony\Component\Yaml\Yaml; diff --git a/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php similarity index 90% rename from databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php rename to databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index 17c83aebc..f985625cd 100644 --- a/databox/api/src/documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -1,6 +1,8 @@ Date: Mon, 7 Jul 2025 12:08:36 +0200 Subject: [PATCH 21/28] add multi-chapters (recursive), with numbered levels change test "about" to 'en' --- .../Command/DocumentationDumperCommand.php | 42 ++++++++++++---- .../Documentation/DocumentationGenerator.php | 41 +++++++++++++++ .../DocumentationGeneratorInterface.php | 7 +-- .../InitialValuesDocumentationGenerator.php | 19 +++++-- ...RenditionBuilderDocumentationGenerator.php | 11 ++-- .../RootDocumentationGenerator.php | 35 +++++++++++++ .../InitialAttributeValuesResolverData.yaml | 50 ++++++++++--------- 7 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 databox/api/src/Documentation/DocumentationGenerator.php create mode 100644 databox/api/src/Documentation/RootDocumentationGenerator.php diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index a27e7d9c1..88721f8e7 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -11,16 +11,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; -use Symfony\Component\String\Slugger\AsciiSlugger; #[AsCommand('app:documentation:dump')] class DocumentationDumperCommand extends Command { - /** - * @uses InitialValuesDocumentationGenerator - * @uses RenditionBuilderDocumentationGenerator - */ - /** @var array */ private array $chapters = []; @@ -34,11 +28,10 @@ protected function configure(): void { parent::configure(); - $slugger = new AsciiSlugger(); /** @var DocumentationGeneratorInterface $documentation */ foreach ($this->documentations as $documentation) { - $name = strtolower($slugger->slug($documentation::getName())->toString()); - if (isset($this->chapters[$documentation::getName()])) { + $name = $documentation->getName(); + if (isset($this->chapters[$name])) { throw new \LogicException(sprintf('Chapter "%s" is already registered.', $name)); } $this->chapters[$name] = $documentation; @@ -73,7 +66,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $input->setArgument('chapters', array_keys($this->chapters)); } foreach ($input->getArgument('chapters') as $chapter) { - $text = '# '.$this->chapters[$chapter]->getName()."\n".$this->chapters[$chapter]->generate(); + $text = $this->getAsText($this->chapters[$chapter]); if ($outputDir) { $outputFile = rtrim($outputDir, '/').'/'.$chapter.'.md'; file_put_contents($outputFile, $text); @@ -85,4 +78,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + + private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = [1]): string + { + $chapter->setLevels($levels); + $text = ''; + $l = join('.', $levels); + if (null !== ($t = $chapter->getTitle())) { + $text .= '# '.$l.': '.$t."\n"; + } + if (null !== ($t = $chapter->getHeader())) { + $text .= $t."\n"; + } + if (null !== ($t = $chapter->getContent())) { + $text .= $t."\n"; + } + + $n = 1; + foreach ($chapter->getChildren() as $child) { + $subLevels = $levels; + $subLevels[] = $n++; + $text .= $this->getAsText($child, $subLevels); + } + + if (null !== ($t = $chapter->getFooter())) { + $text .= $t."\n"; + } + + return $text; + } } diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php new file mode 100644 index 000000000..7d626863e --- /dev/null +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -0,0 +1,41 @@ +levels = $levels; + } + + public function getLevels(): array + { + return $this->levels; + } + + public function getHeader(): ?string + { + return null; + } + + public function getContent(): ?string + { + return null; + } + + public function getFooter(): ?string + { + return null; + } + + /** DocumentationGeneratorInterface[] */ + public function getChildren(): array + { + return []; + } +} diff --git a/databox/api/src/Documentation/DocumentationGeneratorInterface.php b/databox/api/src/Documentation/DocumentationGeneratorInterface.php index 06b0f1b2d..bb8d2c278 100644 --- a/databox/api/src/Documentation/DocumentationGeneratorInterface.php +++ b/databox/api/src/Documentation/DocumentationGeneratorInterface.php @@ -4,14 +4,9 @@ namespace App\Documentation; -use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; - -#[AutoconfigureTag(self::TAG)] interface DocumentationGeneratorInterface { final public const string TAG = 'documentation_generator'; - public static function getName(): string; - - public function generate(): string; + public function getName(): string; } diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 0d6be4e7e..731b0cf88 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -6,14 +6,19 @@ use Symfony\Component\Yaml\Yaml; -class InitialValuesDocumentationGenerator implements DocumentationGeneratorInterface +class InitialValuesDocumentationGenerator extends DocumentationGenerator { - public static function getName(): string + public function getName(): string + { + return 'initial_attribute_values'; + } + + public function getTitle(): string { return 'Initial Attribute Values'; } - public function generate(): string + public function getContent(): ?string { $n = 0; $output = ''; @@ -26,7 +31,13 @@ public function generate(): string $output .= "\n---\n"; } - $output .= sprintf("## %s\n", $test['about']['title'] ?? ''); + $levels = $this->getLevels(); + $levels[] = $n; + + $output .= sprintf("## %s: %s\n", + join('.', $levels), + $test['about']['title'] ?? '' + ); if ($description = $test['about']['description'] ?? '') { $output .= sprintf("%s\n", $description); } diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index f985625cd..e1bc7f8a9 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -6,18 +6,23 @@ use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation; -class RenditionBuilderDocumentationGenerator implements DocumentationGeneratorInterface +class RenditionBuilderDocumentationGenerator extends DocumentationGenerator { public function __construct(private RenditionBuilderConfigurationDocumentation $renditionBuilderConfigurationDocumentation) { } - public static function getName(): string + public function getName(): string + { + return 'rendition_factory'; + } + + public function getTitle(): string { return 'Rendition Factory'; } - public function generate(): string + public function getContent(): string { return $this->renditionBuilderConfigurationDocumentation->generate(); } diff --git a/databox/api/src/Documentation/RootDocumentationGenerator.php b/databox/api/src/Documentation/RootDocumentationGenerator.php new file mode 100644 index 000000000..b98773504 --- /dev/null +++ b/databox/api/src/Documentation/RootDocumentationGenerator.php @@ -0,0 +1,35 @@ +initialValuesDocumentationGenerator, + $this->renditionBuilderDocumentationGenerator, + ]; + } +} diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index 43230d2c3..7dd861088 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -1,25 +1,29 @@ # shortcuts # # - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. -# if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) +# if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) # - same principle applies for expected/ values (no locale == '_' locale) # # - expected values is an array, possibly with only one element -# if only one value is expected, the plain value can be used (it will test with [value]) +# if only one value is expected, the plain value can be used (it will test with [value]) # # - fieldType defaults to text # # - isMultiple defaults to false # # - possible fieldTypes are: -# boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text +# boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text +# +# - documentation generation +# Tests with an "about" section will be included in the documentation exiftoolVersion: about: - title: 'Vérification du fonctionnement de l''intégration metadata-read' + title: 'Check of integration "metadata-read"' description: - 'Cette lecture permet de s''assurer du bon déroulement du processus d''extraction des métadonnées. - La metadata `ExifTool:ExifToolVersion` est toujours retournée par exiftool.' + 'Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. + integration is functional. + The metadata `ExifTool:ExifToolVersion` in always returned by exiftool.' definitions: exiftoolVersion: initialValues: '{ "type": "metadata", "value": "ExifTool:ExifToolVersion"}' @@ -30,59 +34,59 @@ exiftoolVersion: multi2mono: about: - title: 'Meta multi-valuée -> attribut mono-valué (méthode "metadata")' + title: 'Multi-values metadata -> mono-value attribute ("metadata" method)' description: - 'Les valeurs seront séparées par des points-virgules.' + 'Values will be separated by " ; "' definitions: Keywords: isMultiple: false initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: 'chien ; chat ; oiseau' + Keywords: 'dog ; cat ; bird' multi2multi: about: - title: 'Meta multi-valuée -> attribut multi-valué (méthode "metadata")' + title: 'Multi-values metadata -> multi-values attribute ("metadata" method)' definitions: Keywords: isMultiple: true initialValues: '{"type": "metadata", "value": "IPTC:Keywords"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: ['chien', 'chat', 'oiseau'] + Keywords: ['dog', 'cat', 'bird'] multi2mono_template: about: - title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' - description: 'attention : utilisation erronée de getMetadata(...).value (sans "s")' + title: 'Multi-values metadata -> multi-values attribute ("template" method)' + description: '__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s")' definitions: Keywords: isMultiple: true initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').value }}"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: 'chien ; chat ; oiseau' + Keywords: 'dog ; cat ; bird' multi2multi_template: about: - title: 'Meta multi-valuée -> attribut multi-valué (méthode "template")' - description: 'Utilisation correcte de getMetadata(...).value__s__' + title: 'Multi-values metadata -> multi-values attribute ("template" method)' + description: 'Good usage of `getMetadata(...).values`' definitions: Keywords: isMultiple: true initialValues: '{"type": "template", "value": "{{ file.getMetadata(''IPTC:Keywords'').values | join(''\n'') }}"}' metadata: - 'IPTC:Keywords': ['chien', 'chat', 'oiseau'] + 'IPTC:Keywords': ['dog', 'cat', 'bird'] expected: - Keywords: ['chien', 'chat', 'oiseau'] + Keywords: ['dog', 'cat', 'bird'] mono_multilingue: about: - title: 'mono multilingue' + title: 'Mono-value multilocale' definitions: Copyright: initialValues: @@ -97,7 +101,7 @@ mono_multilingue: multi_multilingue: about: - title: 'multi multilingue' + title: 'Multi-values multilocale' definitions: Keywords: isMultiple: true From 3dc8aae76aef04c101fef82845a2994bc9639e45 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Mon, 7 Jul 2025 15:27:26 +0200 Subject: [PATCH 22/28] add tests and allow empty metadata in tests --- .../InitialValuesDocumentationGenerator.php | 2 +- .../InitialAttributeValuesResolverTest.php | 5 +- .../InitialAttributeValuesResolverData.yaml | 111 ++++++++++++++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 731b0cf88..7cafec74e 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -70,7 +70,7 @@ public function getContent(): ?string $output .= "### ... with file metadata ...\n"; $output .= "| metadata | value(s) |\n"; $output .= "|---|---|\n"; - foreach ($test['metadata'] as $metadataName => $values) { + foreach ($test['metadata'] ?? [] as $metadataName => $values) { $v = is_array($values) ? $values : [$values]; $output .= sprintf("| %s | `%s` |\n", $metadataName, join('` ; `', $v)); } diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index c78b115fd..2777c8411 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -50,7 +50,7 @@ function ($test) { * * @param array $metadata */ - public function testResolveInitialAttributes(array $definitions, array $metadata, array $expected): void + public function testResolveInitialAttributes(array $definitions, ?array $metadata, array $expected): void { $attributeDefinitions = []; foreach ($definitions as $name => $definition) { @@ -145,6 +145,9 @@ private function isNumericArray($a): bool private function conformMetadata($data): array { + if (null === $data) { + return []; + } $conformed = []; $data = is_array($data) ? $data : [$data]; foreach ($data as $key => $value) { diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml index 7dd861088..09d9f6631 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml @@ -116,3 +116,114 @@ multi_multilingue: en: ['dog', 'cat', 'bird'] fr: ['chien', 'chat', 'oiseau'] +unknown_tag: + about: + title: 'Unknown tag' + definitions: + zAttribute: + initialValues: '{ + "type": "template", + "value": "{{ (file.getMetadata(''badTag'').value ?? ''whaat ?'') }}" + }' + metadata: ~ + expected: + zAttribute: 'whaat ?' + +first_metadata: + about: + title: 'First metadata set, with alternate value (1)' + description: 'for this test, only the metadata `IPTC:City` is set.' + definitions: + City: + initialValues: '{ + "type": "template", + "value": "{{ file.getMetadata(''XMP-iptcCore:CreatorCity'').value ?? file.getMetadata(''IPTC:City'').value ?? ''no-city ?'' }}" + }' + metadata: + 'IPTC:City': 'Paris' + expected: + City: 'Paris' + +first_metadata_default: + about: + title: 'First metadata set, with alternate value (2)' + description: 'for this test, no metadata is set, so the default value is used.' + definitions: + City: + initialValues: '{ + "type": "template", + "value": "{{ file.getMetadata(''XMP-iptcCore:CreatorCity'').value ?? file.getMetadata(''IPTC:City'').value ?? ''no-city ?'' }}" + }' + metadata: ~ + expected: + City: 'no-city ?' + +many_metadata_to_mono: + about: + title: 'Many metadata to a mono-value attribute' + description: 'Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required.' + definitions: + CreatorLocation: + initialValues: '{ + "type": "template", + "value": "{{ [file.getMetadata(''XMP-iptcCore:CreatorCity'').value, file.getMetadata(''XMP-iptcCore:CreatorCountry'').value] | join('' - '') }}" + }' + metadata: + 'XMP-iptcCore:CreatorCity': 'Paris' + 'XMP-iptcCore:CreatorCountry': 'France' + expected: + CreatorLocation: 'Paris - France' + +many_metadata_to_multi: + about: + title: 'Many metadata to a multi-values attribute' + description: "_nb_: We fill an array that will be joined by `\\n` to generate __one line per item__ (required).\n + \n + The `merge` filter __requires__ arrays, so `IPTC:Keywords` defaults to `[]` in case there is no keywords in metadata.\n" + definitions: + Keywords: + isMultiple: true + initialValues: '{ + "type": "template", + "value": "{{ ( (file.getMetadata(''IPTC:Keywords'').values ?? []) | merge([file.getMetadata(''IPTC:City'').value, file.getMetadata(''XMP-photoshop:Country'').value ]) ) | join(''\n'') }}" + }' + metadata: + 'IPTC:Keywords': ['Eiffel Tower', 'Seine river'] + 'IPTC:City': 'Paris' + 'XMP-photoshop:Country': 'France' + expected: + Keywords: ['Eiffel Tower', 'Seine river', 'Paris', 'France'] + +code_to_string: + about: + title: 'Transform code to human readable text' + description: 'here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings.' + definitions: + ExposureProgram: + initialValues: '{ + "type": "template", + "value": "{{ [ + ''Not Defined'', ''Manual'', ''Program AE'', + ''Aperture-priority AE'', ''Shutter speed priority AE'', + ''Creative (Slow speed)'', ''Action (High speed)'', ''Portrait'', + ''Landscape'', ''Bulb'' + ][file.getMetadata(''ExifIFD:ExposureProgram'').value] + ?? ''Unknown mode'' + }}" + }' + metadata: + 'ExifIFD:ExposureProgram': 2 + expected: + ExposureProgram: 'Program AE' + +code_to_string_2: + definitions: + ExposureProgram: + initialValues: '{ + "type": "template", + "value": "{{ [ ''a'', ''b'' ][file.getMetadata(''ExifIFD:ExposureProgram'').value] ?? ''z'' }}" + }' + metadata: ~ + expected: + ExposureProgram: 'z' + From bf785bc4843dc15e34f5b8e9c082d10e42b88653 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Jul 2025 16:47:00 +0200 Subject: [PATCH 23/28] move test data provider to src (because used to generate doc) ; add frontmatter header to generated md files ; generated md files can go to subdir --- .../Command/DocumentationDumperCommand.php | 28 +++++++++----- .../Documentation/DocumentationGenerator.php | 8 ++++ .../InitialAttributeValuesResolverData.yaml | 37 ++++++++++--------- .../InitialValuesDocumentationGenerator.php | 19 ++++++---- ...RenditionBuilderDocumentationGenerator.php | 5 +++ .../RootDocumentationGenerator.php | 35 ------------------ .../InitialAttributeValuesResolverTest.php | 5 ++- 7 files changed, 66 insertions(+), 71 deletions(-) rename databox/api/{tests/fixtures/metadata => src/Documentation}/InitialAttributeValuesResolverData.yaml (93%) delete mode 100644 databox/api/src/Documentation/RootDocumentationGenerator.php diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 88721f8e7..0aa5c0184 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -47,9 +47,9 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - $outputDir = $input->getOption('output'); - if ($outputDir && !is_dir($outputDir)) { - $output->writeln(sprintf('Output directory "%s" does not exists.', $outputDir)); + $outputRoot = trim($input->getOption('output')); + if ($outputRoot && !is_dir($outputRoot)) { + $output->writeln(sprintf('Output directory "%s" does not exists.', $outputRoot)); return Command::FAILURE; } @@ -67,8 +67,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int } foreach ($input->getArgument('chapters') as $chapter) { $text = $this->getAsText($this->chapters[$chapter]); - if ($outputDir) { - $outputFile = rtrim($outputDir, '/').'/'.$chapter.'.md'; + if ($outputRoot) { + $outputDir = rtrim($outputRoot, '/'); + if ('' !== ($subDir = $this->chapters[$chapter]->getSubdirectory())) { + $outputDir .= '/'.trim($subDir, " \n\r\t\v\0/"); + } + @mkdir($outputDir, 0777, true); + $outputFile = $outputDir.'/'.$chapter.'.md'; file_put_contents($outputFile, $text); $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); } else { @@ -79,14 +84,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } - private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = [1]): string + private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = []): string { $chapter->setLevels($levels); $text = ''; $l = join('.', $levels); - if (null !== ($t = $chapter->getTitle())) { - $text .= '# '.$l.': '.$t."\n"; - } + + $title = $chapter->getTitle() ?? $chapter->getName(); + $text .= "---\n".$l.($l ? ': ' : '').$title."\n---\n\n"; + if (null !== ($t = $chapter->getHeader())) { $text .= $t."\n"; } @@ -97,7 +103,9 @@ private function getAsText(DocumentationGeneratorInterface $chapter, array $leve $n = 1; foreach ($chapter->getChildren() as $child) { $subLevels = $levels; - $subLevels[] = $n++; + if (!empty($subLevels)) { + $subLevels[] = $n++; + } $text .= $this->getAsText($child, $subLevels); } diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php index 7d626863e..095bbf724 100644 --- a/databox/api/src/Documentation/DocumentationGenerator.php +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -4,6 +4,9 @@ namespace App\Documentation; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +#[AutoconfigureTag(DocumentationGeneratorInterface::TAG)] abstract class DocumentationGenerator implements DocumentationGeneratorInterface { private array $levels = []; @@ -23,6 +26,11 @@ public function getHeader(): ?string return null; } + public function getSubdirectory(): string + { + return ''; + } + public function getContent(): ?string { return null; diff --git a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml similarity index 93% rename from databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml rename to databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml index 09d9f6631..8397fc95e 100644 --- a/databox/api/tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml +++ b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml @@ -1,4 +1,10 @@ -# shortcuts +# this file is used to +# - generate the documentation for the initial values of attributes +# blocks with a "documentation" section will be included in the documentation +# - provide the data for the tests +# blocks with `test: false` will not be included in the tests +# +# syntax and shortcuts # # - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. # if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) @@ -13,12 +19,9 @@ # # - possible fieldTypes are: # boolean, code, collection_path, color, date, date_time, entity, geo_point, html, ip, json, keyword, number, tag, textarea, text -# -# - documentation generation -# Tests with an "about" section will be included in the documentation exiftoolVersion: - about: + documentation: title: 'Check of integration "metadata-read"' description: 'Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. @@ -33,7 +36,7 @@ exiftoolVersion: exiftoolVersion: '12.42' multi2mono: - about: + documentation: title: 'Multi-values metadata -> mono-value attribute ("metadata" method)' description: 'Values will be separated by " ; "' @@ -47,7 +50,7 @@ multi2mono: Keywords: 'dog ; cat ; bird' multi2multi: - about: + documentation: title: 'Multi-values metadata -> multi-values attribute ("metadata" method)' definitions: Keywords: @@ -59,7 +62,7 @@ multi2multi: Keywords: ['dog', 'cat', 'bird'] multi2mono_template: - about: + documentation: title: 'Multi-values metadata -> multi-values attribute ("template" method)' description: '__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s")' definitions: @@ -72,7 +75,7 @@ multi2mono_template: Keywords: 'dog ; cat ; bird' multi2multi_template: - about: + documentation: title: 'Multi-values metadata -> multi-values attribute ("template" method)' description: 'Good usage of `getMetadata(...).values`' definitions: @@ -85,7 +88,7 @@ multi2multi_template: Keywords: ['dog', 'cat', 'bird'] mono_multilingue: - about: + documentation: title: 'Mono-value multilocale' definitions: Copyright: @@ -100,7 +103,7 @@ mono_multilingue: fr: '(c) Bob. Tous droits réservés' multi_multilingue: - about: + documentation: title: 'Multi-values multilocale' definitions: Keywords: @@ -117,7 +120,7 @@ multi_multilingue: fr: ['chien', 'chat', 'oiseau'] unknown_tag: - about: + documentation: title: 'Unknown tag' definitions: zAttribute: @@ -130,7 +133,7 @@ unknown_tag: zAttribute: 'whaat ?' first_metadata: - about: + documentation: title: 'First metadata set, with alternate value (1)' description: 'for this test, only the metadata `IPTC:City` is set.' definitions: @@ -145,7 +148,7 @@ first_metadata: City: 'Paris' first_metadata_default: - about: + documentation: title: 'First metadata set, with alternate value (2)' description: 'for this test, no metadata is set, so the default value is used.' definitions: @@ -159,7 +162,7 @@ first_metadata_default: City: 'no-city ?' many_metadata_to_mono: - about: + documentation: title: 'Many metadata to a mono-value attribute' description: 'Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required.' definitions: @@ -175,7 +178,7 @@ many_metadata_to_mono: CreatorLocation: 'Paris - France' many_metadata_to_multi: - about: + documentation: title: 'Many metadata to a multi-values attribute' description: "_nb_: We fill an array that will be joined by `\\n` to generate __one line per item__ (required).\n \n @@ -195,7 +198,7 @@ many_metadata_to_multi: Keywords: ['Eiffel Tower', 'Seine river', 'Paris', 'France'] code_to_string: - about: + documentation: title: 'Transform code to human readable text' description: 'here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings.' definitions: diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 7cafec74e..4d2f2f41c 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -18,12 +18,17 @@ public function getTitle(): string return 'Initial Attribute Values'; } + public function getSubdirectory(): string + { + return 'Databox/Attributes'; + } + public function getContent(): ?string { $n = 0; $output = ''; - foreach (Yaml::parseFile(__DIR__.'/../../tests/fixtures/metadata/InitialAttributeValuesResolverData.yaml') as $test) { - if (!($test['about'] ?? false)) { + foreach (Yaml::parseFile(__DIR__.'/InitialAttributeValuesResolverData.yaml') as $example) { + if (!($example['documentation'] ?? false)) { continue; } @@ -36,14 +41,14 @@ public function getContent(): ?string $output .= sprintf("## %s: %s\n", join('.', $levels), - $test['about']['title'] ?? '' + $example['documentation']['title'] ?? '' ); - if ($description = $test['about']['description'] ?? '') { + if ($description = $example['documentation']['description'] ?? '') { $output .= sprintf("%s\n", $description); } $output .= "### Attribute(s) definition(s) ...\n"; - foreach ($test['definitions'] as $name => $definition) { + foreach ($example['definitions'] as $name => $definition) { $output .= sprintf("- __%s__: `%s` ; [%s] `multi` ; [%s] `translatable`\n", $name, $definition['fieldType'] ?? 'text', @@ -70,7 +75,7 @@ public function getContent(): ?string $output .= "### ... with file metadata ...\n"; $output .= "| metadata | value(s) |\n"; $output .= "|---|---|\n"; - foreach ($test['metadata'] ?? [] as $metadataName => $values) { + foreach ($example['metadata'] ?? [] as $metadataName => $values) { $v = is_array($values) ? $values : [$values]; $output .= sprintf("| %s | `%s` |\n", $metadataName, join('` ; `', $v)); } @@ -78,7 +83,7 @@ public function getContent(): ?string $output .= "\n"; $output .= "### ... set attribute(s) initial value(s)\n"; - $this->dumpExpected($output, $test['expected'], 1); + $this->dumpExpected($output, $example['expected'], 1); $output .= "\n"; } diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index e1bc7f8a9..2141d40f1 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -22,6 +22,11 @@ public function getTitle(): string return 'Rendition Factory'; } + public function getSubdirectory(): string + { + return 'Databox/Renditions'; + } + public function getContent(): string { return $this->renditionBuilderConfigurationDocumentation->generate(); diff --git a/databox/api/src/Documentation/RootDocumentationGenerator.php b/databox/api/src/Documentation/RootDocumentationGenerator.php deleted file mode 100644 index b98773504..000000000 --- a/databox/api/src/Documentation/RootDocumentationGenerator.php +++ /dev/null @@ -1,35 +0,0 @@ -initialValuesDocumentationGenerator, - $this->renditionBuilderDocumentationGenerator, - ]; - } -} diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 2777c8411..10d3670d9 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -37,9 +37,10 @@ function ($test) { ]; }, array_filter( - Yaml::parseFile(__DIR__.'/../../fixtures/metadata/InitialAttributeValuesResolverData.yaml'), + // the data file is used for documentation generation AND as a data provider for tests + Yaml::parseFile(__DIR__.'/../../../src/Documentation/InitialAttributeValuesResolverData.yaml'), function ($test) { - return $test['enabled'] ?? true; + return $test['test'] ?? true; } ) ); From 586921643ff672f472ca8e0bc2ade17e368f27eb Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Jul 2025 19:06:18 +0200 Subject: [PATCH 24/28] remove app:documentation:dump options --- .../Command/DocumentationDumperCommand.php | 57 +++++++------------ .../Documentation/DocumentationGenerator.php | 5 -- .../InitialValuesDocumentationGenerator.php | 7 +-- ...RenditionBuilderDocumentationGenerator.php | 7 +-- 4 files changed, 21 insertions(+), 55 deletions(-) diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index 0aa5c0184..a16b0c27c 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -7,7 +7,6 @@ use App\Documentation\DocumentationGeneratorInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; @@ -39,46 +38,22 @@ protected function configure(): void $this ->setDescription('Dump code-generated documentation(s)') - ->addArgument('chapters', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Chapter(s) to dump. If not specified, all chapters will be dumped.') - ->addOption('output', 'o', InputArgument::OPTIONAL, 'Output directory to write the documentation to. If not specified, it will be written to stdout.') - ->setHelp(sprintf('chapters: "%s"', join('", "', array_keys($this->chapters)))) + ->setHelp(sprintf('chapters: %s', join(' ; ', array_keys($this->chapters)))) ; } protected function execute(InputInterface $input, OutputInterface $output): int { - $outputRoot = trim($input->getOption('output')); - if ($outputRoot && !is_dir($outputRoot)) { - $output->writeln(sprintf('Output directory "%s" does not exists.', $outputRoot)); + foreach (array_keys($this->chapters) as $chapter) { + $pathParts = explode('/', trim($this->chapters[$chapter]->getName(), " \n\r\t\v\0/")); + $filename = array_pop($pathParts); - return Command::FAILURE; - } - - foreach ($input->getArgument('chapters') as $chapter) { - if (!isset($this->chapters[$chapter])) { - $output->writeln(sprintf('Unknown chapter "%s". Available chapters are "%s"', $chapter, join('", "', array_keys($this->chapters)))); + $outputDir = '../../doc/'.join('/', $pathParts); + @mkdir($outputDir, 0777, true); + $outputFile = $outputDir.'/'.$filename.'.md'; - return Command::FAILURE; - } - } - - if (empty($input->getArgument('chapters'))) { - $input->setArgument('chapters', array_keys($this->chapters)); - } - foreach ($input->getArgument('chapters') as $chapter) { - $text = $this->getAsText($this->chapters[$chapter]); - if ($outputRoot) { - $outputDir = rtrim($outputRoot, '/'); - if ('' !== ($subDir = $this->chapters[$chapter]->getSubdirectory())) { - $outputDir .= '/'.trim($subDir, " \n\r\t\v\0/"); - } - @mkdir($outputDir, 0777, true); - $outputFile = $outputDir.'/'.$chapter.'.md'; - file_put_contents($outputFile, $text); - $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); - } else { - $output->writeln($text); - } + file_put_contents($outputFile, $this->getAsText($this->chapters[$chapter])); + $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); } return Command::SUCCESS; @@ -87,11 +62,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = []): string { $chapter->setLevels($levels); - $text = ''; - $l = join('.', $levels); - $title = $chapter->getTitle() ?? $chapter->getName(); - $text .= "---\n".$l.($l ? ': ' : '').$title."\n---\n\n"; + $title = str_replace("'", "''", $chapter->getTitle() ?? $chapter->getName()); // Escape single quotes for YAML frontmatter + if (!empty($levels)) { + $title = join('.', $levels).': '.$title; + } + $frontmatter = "---\n" + ."title: '".$title."'\n" + ."comment: 'Generated by the DocumentationDumperCommand, do not modify'\n" + ."---\n"; + + $text = $frontmatter."\n"; if (null !== ($t = $chapter->getHeader())) { $text .= $t."\n"; diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php index 095bbf724..118319336 100644 --- a/databox/api/src/Documentation/DocumentationGenerator.php +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -26,11 +26,6 @@ public function getHeader(): ?string return null; } - public function getSubdirectory(): string - { - return ''; - } - public function getContent(): ?string { return null; diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index 4d2f2f41c..f949a9ff4 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -10,7 +10,7 @@ class InitialValuesDocumentationGenerator extends DocumentationGenerator { public function getName(): string { - return 'initial_attribute_values'; + return 'Databox/Attributes/initial_attribute_values'; } public function getTitle(): string @@ -18,11 +18,6 @@ public function getTitle(): string return 'Initial Attribute Values'; } - public function getSubdirectory(): string - { - return 'Databox/Attributes'; - } - public function getContent(): ?string { $n = 0; diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index 2141d40f1..a8b0826b9 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -14,7 +14,7 @@ public function __construct(private RenditionBuilderConfigurationDocumentation $ public function getName(): string { - return 'rendition_factory'; + return 'Databox/Renditions/rendition_factory'; } public function getTitle(): string @@ -22,11 +22,6 @@ public function getTitle(): string return 'Rendition Factory'; } - public function getSubdirectory(): string - { - return 'Databox/Renditions'; - } - public function getContent(): string { return $this->renditionBuilderConfigurationDocumentation->generate(); From 68a42296abe8090a098e4c351061658ff7eaadcd Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Jul 2025 19:41:39 +0200 Subject: [PATCH 25/28] add generated doc to git --- .../Attributes/initial_attribute_values.md | 350 ++++++++++++ doc/Databox/Renditions/rendition_factory.md | 531 ++++++++++++++++++ 2 files changed, 881 insertions(+) create mode 100644 doc/Databox/Attributes/initial_attribute_values.md create mode 100644 doc/Databox/Renditions/rendition_factory.md diff --git a/doc/Databox/Attributes/initial_attribute_values.md b/doc/Databox/Attributes/initial_attribute_values.md new file mode 100644 index 000000000..9790b60d1 --- /dev/null +++ b/doc/Databox/Attributes/initial_attribute_values.md @@ -0,0 +1,350 @@ +--- +title: 'Initial Attribute Values' +comment: 'Generated by the DocumentationDumperCommand, do not modify' +--- + +## 1: Check of integration "metadata-read" +Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. integration is functional. The metadata `ExifTool:ExifToolVersion` in always returned by exiftool. +### Attribute(s) definition(s) ... +- __exiftoolVersion__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "metadata", + "value": "ExifTool:ExifToolVersion" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| ExifTool:ExifToolVersion | `12.42` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | exiftoolVersion | `12.42` + + +--- +## 2: Multi-values metadata -> mono-value attribute ("metadata" method) +Values will be separated by " ; " +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "metadata", + "value": "IPTC:Keywords" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords | `dog ; cat ; bird` + + +--- +## 3: Multi-values metadata -> multi-values attribute ("metadata" method) +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "metadata", + "value": "IPTC:Keywords" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords #1 | `dog` | + | Keywords #2 | `cat` | + | Keywords #3 | `bird` | + + +--- +## 4: Multi-values metadata -> multi-values attribute ("template" method) +__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s") +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('IPTC:Keywords').value }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords | `dog ; cat ; bird` + + +--- +## 5: Multi-values metadata -> multi-values attribute ("template" method) +Good usage of `getMetadata(...).values` +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('IPTC:Keywords').values | join('\n') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `dog` ; `cat` ; `bird` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords #1 | `dog` | + | Keywords #2 | `cat` | + | Keywords #3 | `bird` | + + +--- +## 6: Mono-value multilocale +### Attribute(s) definition(s) ... +- __Copyright__: `text` ; [ ] `multi` ; [ ] `translatable` + + - locale `en` + ```json + { + "type": "template", + "value": "(c) {{ file.getMetadata('XMP-dc:Creator').value }}. All rights reserved" + } + ``` + - locale `fr` + ```json + { + "type": "template", + "value": "(c) {{ file.getMetadata('XMP-dc:Creator').value }}. Tous droits réservés" + } + ``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| XMP-dc:Creator | `Bob` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | __locale `en`__ | | + | Copyright | `(c) Bob. All rights reserved` | + | __locale `fr`__ | | + | Copyright | `(c) Bob. Tous droits réservés` | + + +--- +## 7: Multi-values multilocale +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + + - locale `en` + ```json + { + "type": "metadata", + "value": "XMP-dc:Subject" + } + ``` + - locale `fr` + ```json + { + "type": "metadata", + "value": "IPTC:SupplementalCategories" + } + ``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| XMP-dc:Subject | `dog` ; `cat` ; `bird` | +| IPTC:SupplementalCategories | `chien` ; `chat` ; `oiseau` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | __locale `en`__ | | + | Keywords #1 | `dog` | + | Keywords #2 | `cat` | + | Keywords #3 | `bird` | + | __locale `fr`__ | | + | Keywords #1 | `chien` | + | Keywords #2 | `chat` | + | Keywords #3 | `oiseau` | + + +--- +## 8: Unknown tag +### Attribute(s) definition(s) ... +- __zAttribute__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ (file.getMetadata('badTag').value ?? 'whaat ?') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | zAttribute | `whaat ?` + + +--- +## 9: First metadata set, with alternate value (1) +for this test, only the metadata `IPTC:City` is set. +### Attribute(s) definition(s) ... +- __City__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('XMP-iptcCore:CreatorCity').value ?? file.getMetadata('IPTC:City').value ?? 'no-city ?' }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:City | `Paris` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | City | `Paris` + + +--- +## 10: First metadata set, with alternate value (2) +for this test, no metadata is set, so the default value is used. +### Attribute(s) definition(s) ... +- __City__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ file.getMetadata('XMP-iptcCore:CreatorCity').value ?? file.getMetadata('IPTC:City').value ?? 'no-city ?' }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | City | `no-city ?` + + +--- +## 11: Many metadata to a mono-value attribute +Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required. +### Attribute(s) definition(s) ... +- __CreatorLocation__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ [file.getMetadata('XMP-iptcCore:CreatorCity').value, file.getMetadata('XMP-iptcCore:CreatorCountry').value] | join(' - ') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| XMP-iptcCore:CreatorCity | `Paris` | +| XMP-iptcCore:CreatorCountry | `France` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | CreatorLocation | `Paris - France` + + +--- +## 12: Many metadata to a multi-values attribute +_nb_: We fill an array that will be joined by `\n` to generate __one line per item__ (required). + + The `merge` filter __requires__ arrays, so `IPTC:Keywords` defaults to `[]` in case there is no keywords in metadata. + +### Attribute(s) definition(s) ... +- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ ( (file.getMetadata('IPTC:Keywords').values ?? []) | merge([file.getMetadata('IPTC:City').value, file.getMetadata('XMP-photoshop:Country').value ]) ) | join('\n') }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| IPTC:Keywords | `Eiffel Tower` ; `Seine river` | +| IPTC:City | `Paris` | +| XMP-photoshop:Country | `France` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | Keywords #1 | `Eiffel Tower` | + | Keywords #2 | `Seine river` | + | Keywords #3 | `Paris` | + | Keywords #4 | `France` | + + +--- +## 13: Transform code to human readable text +here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings. +### Attribute(s) definition(s) ... +- __ExposureProgram__: `text` ; [ ] `multi` ; [ ] `translatable` + +```json +{ + "type": "template", + "value": "{{ [ 'Not Defined', 'Manual', 'Program AE', 'Aperture-priority AE', 'Shutter speed priority AE', 'Creative (Slow speed)', 'Action (High speed)', 'Portrait', 'Landscape', 'Bulb' ][file.getMetadata('ExifIFD:ExposureProgram').value] ?? 'Unknown mode' }}" +} +``` + +### ... with file metadata ... +| metadata | value(s) | +|---|---| +| ExifIFD:ExposureProgram | `2` | + +### ... set attribute(s) initial value(s) + | Attributes | initial value(s) | + |---|---| + | ExposureProgram | `Program AE` + + diff --git a/doc/Databox/Renditions/rendition_factory.md b/doc/Databox/Renditions/rendition_factory.md new file mode 100644 index 000000000..5b007ad9e --- /dev/null +++ b/doc/Databox/Renditions/rendition_factory.md @@ -0,0 +1,531 @@ +--- +title: 'Rendition Factory' +comment: 'Generated by the DocumentationDumperCommand, do not modify' +--- + +## `imagine` transformer module +Transform an image with some filter. +```yaml +- + module: imagine # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Output image format + format: ~ # Example: jpeg + # Filters to apply to the image + filters: + # Filter performs sizing transformations (specifically relative resizing) + relative_resize: + heighten: ~ # Example: 'value "60" => given 50x40px, output 75x60px using "heighten" option' + widen: ~ # Example: 'value "32" => given 50x40px, output 32x26px using "widen" option' + increase: ~ # Example: 'value "10" => given 50x40px, output 60x50px, using "increase" option' + scale: ~ # Example: 'value "2.5" => given 50x40px, output 125x100px using "scale" option' + # use and setup the resize filter + resize: + # set the size of the resizing area [width,height] + size: + 0: ~ # Example: '120' + 1: ~ # Example: '90' + # Filter performs thumbnail transformations (which includes scaling and potentially cropping operations) + thumbnail: + # set the thumbnail size to [width,height] pixels + size: + 0: ~ # Example: '32' + 1: ~ # Example: '32' + # Sets the desired resize method: "outbound" crops the image as required, while "inset" performs a non-cropping relative resize. + mode: ~ # Example: inset + # Toggles allowing image up-scaling when the image is smaller than the desired thumbnail size. Value: true or false + allow_upscale: ~ + # filter performs sizing transformations (which includes cropping operations) + crop: + # set the size of the cropping area [width,height] + size: + 0: ~ # Example: '300' + 1: ~ # Example: '600' + # Sets the top, left-post anchor coordinates where the crop operation starts[x, y] + start: + 0: ~ # Example: '32' + 1: ~ # Example: '160' + # filter adds a watermark to an existing image + watermark: + # Path to the watermark image + image: ~ + # filter fill background color + background_fill: + # Sets the background color HEX value. The default color is white (#fff). + color: ~ + # Sets the background opacity. The value should be within a range of 0 (fully transparent) - 100 (opaque). default opacity 100 + opacity: ~ + # filter performs file transformations (which includes metadata removal) + strip: ~ + # filter performs sizing transformations (specifically image scaling) + scale: + # Sets the "desired dimensions" [width, height], from which a relative resize is performed within these constraints. + dim: + 0: ~ # Example: '800' + 1: ~ # Example: '1000' + # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. + to: ~ # Example: '1.5' + # filter performs sizing transformations (specifically image up-scaling) + upscale: + # Sets the "desired min dimensions" [width, height], from which an up-scale is performed to meet the passed constraints. + min: + 0: ~ # Example: '1200' + 1: ~ # Example: '800' + # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. + by: ~ # Example: '0.7' + # filter performs sizing transformations (specifically image down-scaling) + downscale: + # Sets the "desired max dimensions" [width, height], from which a down-scale is performed to meet the passed constraints + max: + 0: ~ # Example: '1980' + 1: ~ # Example: '1280' + # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. + by: ~ # Example: '0.6' + # filter performs orientation transformations (which includes rotating the image) + auto_rotate: ~ + # filter performs orientation transformations (specifically image rotation) + rotate: + # Sets the rotation angle in degrees. The default value is 0. + angle: ~ # Example: '90' + # filter performs orientation transformations (specifically image flipping) + flip: + # Sets the "flip axis" that defines the axis on which to flip the image. Valid values: x, horizontal, y, vertical + axis: ~ # Example: x + # filter performs file transformations (which includes modifying the encoding method) + interlace: + # Sets the interlace mode to encode the file with. Valid values: none, line, plane, and partition. + mode: ~ # Example: line + # filter provides a resampling transformation by allows you to change the resolution of an image + resample: + # Sets the unit to use for pixel density, either "pixels per inch" or "pixels per centimeter". Valid values: ppi and ppc + unit: ~ # Example: ppi + # Sets the horizontal resolution in the specified unit + x: ~ # Example: '300' + # Sets the vertical resolution in the specified unit + y: ~ # Example: '200' + # Sets the optional temporary work directory. This filter requires a temporary location to save out and read back in the image binary, as these operations are requires to resample an image. By default, it is set to the value of the sys_get_temp_dir() function + tmp_dir: ~ # Example: /my/custom/temporary/directory/path + # filter performs thumbnail transformations (which includes scaling and potentially cropping operations) + fixed: + # Sets the "desired width" which initiates a proportional scale operation that up- or down-scales until the image width matches this value. + width: ~ # Example: '120' + # Sets the "desired height" which initiates a proportional scale operation that up- or down-scales until the image height matches this value + height: ~ # Example: '90' + # use stamp + stamp: + # path to the font file ttf + font: ~ + # available position value: topleft, top, topright, left, center, right, bottomleft, bottom, bottomright, under, above + position: ~ + angle: 0 + # font size + size: 16 + color: '#000000' + # the font alpha value + alpha: 100 + # text to stamp + text: ~ + # text width + width: ~ + # text background, option use for position under or above + background: '#FFFFFF' + # background transparancy, option use for position under or above + transparency: null +``` +## `void` transformer module +A module that does nothing (testing purpose) +```yaml +- + module: void # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true +``` +## `video_summary` transformer module +Assemble multiple extracts (clips) of the video. +```yaml +- + module: video_summary # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Skip video start, in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' + # Extract one video clip every period, in seconds or timecode + period: ~ # Required, Example: '5 ; "00:00:05.00"' + # Duration of each clip, in seconds or timecode + duration: ~ # Required, Example: '0.25 ; "00:00:00.25"' + # output format + format: ~ # Required, Example: video-mpeg + # extension of the output file + extension: 'default extension from format' # Example: mpeg + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| video |||| +|| video-mkv | video/x-matroska | mkv | +|| video-mpeg4 | video/mp4 | mp4 | +|| video-mpeg | video/mpeg | mpeg | +|| video-quicktime | video/quicktime | mov | +|| video-webm | video/webm | webm | +|| video-ogg | video/ogg | ogv | + +## `ffmpeg` transformer module +apply filters to a video using FFMpeg. +```yaml +- + module: ffmpeg # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # output format + format: ~ + # extension of the output file + extension: ~ + # Change the default video codec used by the output format + video_codec: ~ + # Change the default audio codec used by the output format + audio_codec: ~ + # Change the default video_kilobitrate used by the output format + video_kilobitrate: ~ + # Change the default audio_kilobitrate used by the output format + audio_kilobitrate: ~ + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ + # Filters to apply to the video + filters: + # Prototype: see list of available filters below + - + # Name of the filter + name: ~ # Required + # Whether to enable the filter + enabled: true +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| audio |||| +|| audio-wav | audio/wav | wav | +|| audio-aac | audio/aac | aac, m4a | +|| audio-mp3 | audio/mp3 | mp3 | +|| audio-ogg | audio/ogg | oga, ogg | +| image |||| +|| image-jpeg | image/jpeg | jpg, jpeg | +|| image-gif | image/gif | gif | +|| image-png | image/png | png | +|| image-tiff | image/tiff | tif, tiff | +| video |||| +|| video-mkv | video/x-matroska | mkv | +|| video-mpeg4 | video/mp4 | mp4 | +|| video-mpeg | video/mpeg | mpeg | +|| video-quicktime | video/quicktime | mov | +|| video-webm | video/webm | webm | +|| video-ogg | video/ogg | ogv | +### List of `ffmpeg` filters: +- `pre_clip` filter +```yaml +# Clip the video before applying other filters +- + name: pre_clip # Required + enabled: true + # Offset of frame in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.500" ; "{{ attr.start }}"' + # Duration in seconds or timecode + duration: null # Example: '30 ; "00:00:30" ; "{{ input.duration/2 }}"' +``` +- `clip` filter +```yaml +# Clip the video or audio +- + name: clip # Required + enabled: true + # Offset of frame in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.500" ; "{{ attr.start }}"' + # Duration in seconds or timecode + duration: null # Example: '30 ; "00:00:30" ; "{{ input.duration/2 }}"' +``` +- `remove_audio` filter +```yaml +# Remove the audio from the video +- + name: remove_audio # Required + enabled: true +``` +- `resample_audio` filter +```yaml +# Resample the audio +- + name: resample_audio # Required + enabled: true + rate: '44100' # Required +``` +- `resize` filter +```yaml +# Resize the video +- + name: resize # Required + enabled: true + # Width of the video + width: ~ # Required + # Height of the video + height: ~ # Required + # Resize mode + mode: inset # Example: inset + # Correct the width/height to the closest "standard" size + force_standards: true +``` +- `rotate` filter +```yaml +# Rotate the video +- + name: rotate # Required + enabled: true + # Angle of rotation [0 | 90 | 180 | 270] + angle: ~ # Required, Example: '90' +``` +- `pad` filter +```yaml +# Pad the video +- + name: pad # Required + enabled: true + # Width of the video + width: ~ # Required + # Height of the video + height: ~ # Required +``` +- `crop` filter +```yaml +# Crop the video +- + name: crop # Required + enabled: true + # X coordinate + x: ~ # Required + # Y coordinate + y: ~ # Required + # Width of the video + width: ~ # Required + # Height of the video + height: ~ # Required +``` +- `watermark` filter +```yaml +# Apply a watermark on the video +- + name: watermark # Required + enabled: true + # "relative" or "absolute" position + position: ~ # Required + # Path to the watermark image + path: ~ # Required + # top coordinate (only if position is "relative", set top OR bottom) + top: ~ + # bottom coordinate (only if position is "relative", set top OR bottom) + bottom: ~ + # left coordinate (only if position is "relative", set left OR right) + left: ~ + # right coordinate (only if position is "relative", set left OR right) + right: ~ + # X coordinate (only if position is "absolute") + x: ~ + # Y coordinate (only if position is "absolute") + y: ~ +``` +- `framerate` filter +```yaml +# Change the framerate +- + name: framerate # Required + enabled: true + # framerate + framerate: ~ # Required + # gop + gop: ~ +``` +- `synchronize` filter +```yaml +# re-synchronize audio and video +- + name: synchronize # Required + enabled: true +``` +## `video_to_frame` transformer module +Extract one frame from the video. +```yaml +- + module: video_to_frame # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Offset of frame in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' + # output format + format: ~ # Required, Example: image-jpeg + # extension of the output file + extension: 'default extension from format' # Example: jpg + # Change the quality of the output file (0-100) + quality: 80 + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| image |||| +|| image-jpeg | image/jpeg | jpg, jpeg | +|| image-gif | image/gif | gif | +|| image-png | image/png | png | +|| image-tiff | image/tiff | tif, tiff | + +## `video_to_animation` transformer module +Converts a video to an animated GIF / PNG. +```yaml +- + module: video_to_animation # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Start time in seconds or timecode + start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' + # Duration in seconds or timecode + duration: null # Example: '30 ; "00:00:30.00" ; "{{ input.duration/2 }}"' + # Frames per second + fps: 1 + # Width in pixels + width: null + # Height in pixels + height: null + # Resize mode + mode: inset # One of "inset" + # output format + format: ~ # Required, Example: animated-png + # extension of the output file + extension: 'default extension from format' # Example: apng + # Change the number of ffmpeg passes + passes: 2 + # Change the default timeout used by ffmpeg (defaults to symphony process timeout) + timeout: ~ + # Change the default number of threads used by ffmpeg + threads: ~ +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| animation |||| +|| animated-gif | image/gif | gif | +|| animated-png | image/apng | apng, png | +|| animated-webp | image/webp | webp | + +## `album_artwork` transformer module +Extract the artwork (cover) of an audio file. +```yaml +- + module: album_artwork # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # output format + format: ~ # Required, Example: image-jpeg + # extension of the output file + extension: 'default extension from format' # Example: jpg +``` +### Supported output `format`s. +| Family | Format | Mime type | Extensions | +|-|-|-|-| +| image |||| +|| image-jpeg | image/jpeg | jpg, jpeg | +|| image-gif | image/gif | gif | +|| image-png | image/png | png | +|| image-tiff | image/tiff | tif, tiff | + +## `document_to_pdf` transformer module +Convert any document to PDF format. +```yaml +- + module: document_to_pdf # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: [] +``` +## `pdf_to_image` transformer module +Convert the first page of a PDF to an image. +```yaml +- + module: pdf_to_image # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # Output image extension: jpg, jpeg, png, or webp + extension: jpeg + # Resolution of the output image in dpi + resolution: 300 + # Quality of the output image, from 0 to 100 + quality: 100 + # Size of the output image, [width, height] in pixels + size: + # Width of the output image in pixels + 0: ~ # Example: '150' + # Height of the output image in pixels + 1: ~ # Example: '100' +``` +## `set_dpi` transformer module +Change the dpi metadata of an image (no resampling). +```yaml +- + module: set_dpi # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + dpi: ~ # Required, Example: '72' +``` +## `download` transformer module +Download a file to be used as output. +```yaml +- + module: download # Required + # Description of the module action + description: ~ + # Whether to enable this module + enabled: true + options: + # url of the file to download + url: ~ +``` + From 72696581b324bd1c17b8ed0d90d3fea26bfd13c2 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Tue, 22 Jul 2025 10:32:59 +0200 Subject: [PATCH 26/28] apply review --- .../api/src/Attribute/AttributeAssigner.php | 2 +- .../Command/DocumentationDumperCommand.php | 19 +++++++------- .../Documentation/DocumentationGenerator.php | 5 +--- .../DocumentationGeneratorInterface.php | 18 ++++++++++++- .../InitialAttributeValuesResolverData.yaml | 4 +-- .../InitialValuesDocumentationGenerator.php | 2 +- ...RenditionBuilderDocumentationGenerator.php | 2 +- .../InitialAttributeValuesResolverTest.php | 26 +++++++++---------- 8 files changed, 46 insertions(+), 32 deletions(-) diff --git a/databox/api/src/Attribute/AttributeAssigner.php b/databox/api/src/Attribute/AttributeAssigner.php index 8135e9645..af2cd03e9 100644 --- a/databox/api/src/Attribute/AttributeAssigner.php +++ b/databox/api/src/Attribute/AttributeAssigner.php @@ -10,7 +10,7 @@ use App\Entity\Core\AbstractBaseAttribute; use App\Entity\Core\Attribute; -class AttributeAssigner +final readonly class AttributeAssigner { public function __construct(private AttributeTypeRegistry $attributeTypeRegistry) { diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index a16b0c27c..dd88c87ea 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -29,11 +29,11 @@ protected function configure(): void /** @var DocumentationGeneratorInterface $documentation */ foreach ($this->documentations as $documentation) { - $name = $documentation->getName(); - if (isset($this->chapters[$name])) { - throw new \LogicException(sprintf('Chapter "%s" is already registered.', $name)); + $k = $documentation->getPath(); + if (isset($this->chapters[$k])) { + throw new \LogicException(sprintf('Chapter "%s" is already registered.', $k)); } - $this->chapters[$name] = $documentation; + $this->chapters[$k] = $documentation; } $this @@ -44,16 +44,17 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { - foreach (array_keys($this->chapters) as $chapter) { - $pathParts = explode('/', trim($this->chapters[$chapter]->getName(), " \n\r\t\v\0/")); + foreach ($this->chapters as $chapter) { + $title = $chapter->getTitle() ?? $chapter->getPath(); + $pathParts = explode('/', trim($chapter->getPath(), " \n\r\t\v\0/")); $filename = array_pop($pathParts); $outputDir = '../../doc/'.join('/', $pathParts); @mkdir($outputDir, 0777, true); $outputFile = $outputDir.'/'.$filename.'.md'; - file_put_contents($outputFile, $this->getAsText($this->chapters[$chapter])); - $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $chapter, $outputFile)); + file_put_contents($outputFile, $this->getAsText($chapter)); + $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $title, $outputFile)); } return Command::SUCCESS; @@ -63,7 +64,7 @@ private function getAsText(DocumentationGeneratorInterface $chapter, array $leve { $chapter->setLevels($levels); - $title = str_replace("'", "''", $chapter->getTitle() ?? $chapter->getName()); // Escape single quotes for YAML frontmatter + $title = str_replace("'", "''", $chapter->getTitle() ?? $chapter->getPath()); // Escape single quotes for YAML frontmatter if (!empty($levels)) { $title = join('.', $levels).': '.$title; } diff --git a/databox/api/src/Documentation/DocumentationGenerator.php b/databox/api/src/Documentation/DocumentationGenerator.php index 118319336..1683ce834 100644 --- a/databox/api/src/Documentation/DocumentationGenerator.php +++ b/databox/api/src/Documentation/DocumentationGenerator.php @@ -4,9 +4,6 @@ namespace App\Documentation; -use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; - -#[AutoconfigureTag(DocumentationGeneratorInterface::TAG)] abstract class DocumentationGenerator implements DocumentationGeneratorInterface { private array $levels = []; @@ -16,7 +13,7 @@ final public function setLevels(array $levels): void $this->levels = $levels; } - public function getLevels(): array + final public function getLevels(): array { return $this->levels; } diff --git a/databox/api/src/Documentation/DocumentationGeneratorInterface.php b/databox/api/src/Documentation/DocumentationGeneratorInterface.php index bb8d2c278..e4ce7fddd 100644 --- a/databox/api/src/Documentation/DocumentationGeneratorInterface.php +++ b/databox/api/src/Documentation/DocumentationGeneratorInterface.php @@ -4,9 +4,25 @@ namespace App\Documentation; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + +#[AutoconfigureTag(DocumentationGeneratorInterface::TAG)] interface DocumentationGeneratorInterface { final public const string TAG = 'documentation_generator'; - public function getName(): string; + public function getPath(): string; + + public function setLevels(array $levels): void; + + public function getLevels(): array; + + public function getHeader(): ?string; + + public function getContent(): ?string; + + public function getFooter(): ?string; + + /** DocumentationGeneratorInterface[] */ + public function getChildren(): array; } diff --git a/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml index 8397fc95e..79d55dbca 100644 --- a/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml +++ b/databox/api/src/Documentation/InitialAttributeValuesResolverData.yaml @@ -1,10 +1,10 @@ -# this file is used to +# This file is used to: # - generate the documentation for the initial values of attributes # blocks with a "documentation" section will be included in the documentation # - provide the data for the tests # blocks with `test: false` will not be included in the tests # -# syntax and shortcuts +# Syntax and shortcuts # # - definitions//initialValues is an array with keys as locale code, like '_', 'fr', 'en', etc. # if no locale distingo is needed, the plain value can be used (it will be converted to ['_' => value]) diff --git a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php index f949a9ff4..7631302f6 100644 --- a/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php +++ b/databox/api/src/Documentation/InitialValuesDocumentationGenerator.php @@ -8,7 +8,7 @@ class InitialValuesDocumentationGenerator extends DocumentationGenerator { - public function getName(): string + public function getPath(): string { return 'Databox/Attributes/initial_attribute_values'; } diff --git a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php index a8b0826b9..4e323b8e8 100644 --- a/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php +++ b/databox/api/src/Documentation/RenditionBuilderDocumentationGenerator.php @@ -12,7 +12,7 @@ public function __construct(private RenditionBuilderConfigurationDocumentation $ { } - public function getName(): string + public function getPath(): string { return 'Databox/Renditions/rendition_factory'; } diff --git a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php index 10d3670d9..4d5c4e0ca 100644 --- a/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php +++ b/databox/api/tests/Asset/Attribute/InitialAttributeValuesResolverTest.php @@ -81,7 +81,7 @@ public function testResolveInitialAttributes(array $definitions, ?array $metadat $fileMock->expects($this->any()) ->method('getMetadata') - ->willReturn($this->conformMetadata($metadata)); + ->willReturn($this->normalizeMetadata($metadata)); $assetMock = $this->createMock(Asset::class); $assetMock->expects($this->any()) @@ -101,20 +101,20 @@ public function testResolveInitialAttributes(array $definitions, ?array $metadat $result[$attribute->getDefinition()->getName()][$attribute->getLocale()][] = $attribute->getValue(); } - $this->assertEquals($this->conformExpected($expected), $result); + $this->assertEquals($this->normalizeExpected($expected), $result); } - private function conformExpected(array $expected): array + private function normalizeExpected(array $expected): array { - $conformed = []; + $normalized = []; foreach ($expected as $attributeName => $value) { if (is_array($value)) { if ($this->isNumericArray($value)) { // a simple list of values - $conformed[$attributeName] = ['_' => $value]; + $normalized[$attributeName] = ['_' => $value]; } else { // an array with key=locale - $conformed[$attributeName] = array_map( + $normalized[$attributeName] = array_map( function ($v) { return is_array($v) ? $v : [$v]; }, @@ -123,11 +123,11 @@ function ($v) { } } else { // a single value - $conformed[$attributeName] = ['_' => [$value]]; + $normalized[$attributeName] = ['_' => [$value]]; } } - return $conformed; + return $normalized; } private function isNumericArray($a): bool @@ -144,27 +144,27 @@ private function isNumericArray($a): bool return true; } - private function conformMetadata($data): array + private function normalizeMetadata($data): array { if (null === $data) { return []; } - $conformed = []; + $normalized = []; $data = is_array($data) ? $data : [$data]; foreach ($data as $key => $value) { if (is_array($value)) { - $conformed[$key] = [ + $normalized[$key] = [ 'value' => join(' ; ', $value), 'values' => $value, ]; } else { - $conformed[$key] = [ + $normalized[$key] = [ 'value' => $value, 'values' => [$value], ]; } } - return $conformed; + return $normalized; } } From c87d32dfa49182cf5eb0ddfe2172fd6bc26129e4 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Mon, 28 Jul 2025 18:20:37 +0200 Subject: [PATCH 27/28] remove generated doc allow generation of non-markdown files ; generates databox-api schema (json) --- .../Command/DocumentationDumperCommand.php | 40 +- .../Attributes/initial_attribute_values.md | 350 ------------ doc/Databox/Renditions/rendition_factory.md | 531 ------------------ 3 files changed, 28 insertions(+), 893 deletions(-) delete mode 100644 doc/Databox/Attributes/initial_attribute_values.md delete mode 100644 doc/Databox/Renditions/rendition_factory.md diff --git a/databox/api/src/Command/DocumentationDumperCommand.php b/databox/api/src/Command/DocumentationDumperCommand.php index dd88c87ea..fe68abe76 100644 --- a/databox/api/src/Command/DocumentationDumperCommand.php +++ b/databox/api/src/Command/DocumentationDumperCommand.php @@ -46,21 +46,27 @@ protected function execute(InputInterface $input, OutputInterface $output): int { foreach ($this->chapters as $chapter) { $title = $chapter->getTitle() ?? $chapter->getPath(); - $pathParts = explode('/', trim($chapter->getPath(), " \n\r\t\v\0/")); - $filename = array_pop($pathParts); + $pathParts = pathinfo($chapter->getPath()); + + $outputDir = '../../doc/'.$pathParts['dirname']; + $filename = $pathParts['filename']; + $extension = $pathParts['extension'] ?? 'md'; + if (!in_array($extension, ['md', 'xml', 'json'], true)) { + $output->writeln(sprintf('Chapter "%s" must have a [md | xml | json] extension, found "%s".', $title, $extension)); + continue; + } - $outputDir = '../../doc/'.join('/', $pathParts); @mkdir($outputDir, 0777, true); - $outputFile = $outputDir.'/'.$filename.'.md'; + $outputFile = $outputDir.'/'.$filename.'.'.$extension; - file_put_contents($outputFile, $this->getAsText($chapter)); + file_put_contents($outputFile, $this->getAsText($chapter, $extension)); $output->writeln(sprintf('Documentation for chapter "%s" written to "%s".', $title, $outputFile)); } return Command::SUCCESS; } - private function getAsText(DocumentationGeneratorInterface $chapter, array $levels = []): string + private function getAsText(DocumentationGeneratorInterface $chapter, string $extension, array $levels = []): string { $chapter->setLevels($levels); @@ -68,12 +74,22 @@ private function getAsText(DocumentationGeneratorInterface $chapter, array $leve if (!empty($levels)) { $title = join('.', $levels).': '.$title; } - $frontmatter = "---\n" - ."title: '".$title."'\n" - ."comment: 'Generated by the DocumentationDumperCommand, do not modify'\n" - ."---\n"; - $text = $frontmatter."\n"; + $text = ''; + switch ($extension) { + case 'md': + $text = "---\n" + ."title: '".$title."'\n" + ."comment: 'Generated by the DocumentationDumperCommand, do not modify'\n" + ."---\n\n"; + break; + case 'xml': + $text = "\n\n"; + break; + } if (null !== ($t = $chapter->getHeader())) { $text .= $t."\n"; @@ -88,7 +104,7 @@ private function getAsText(DocumentationGeneratorInterface $chapter, array $leve if (!empty($subLevels)) { $subLevels[] = $n++; } - $text .= $this->getAsText($child, $subLevels); + $text .= $this->getAsText($child, $extension, $subLevels); } if (null !== ($t = $chapter->getFooter())) { diff --git a/doc/Databox/Attributes/initial_attribute_values.md b/doc/Databox/Attributes/initial_attribute_values.md deleted file mode 100644 index 9790b60d1..000000000 --- a/doc/Databox/Attributes/initial_attribute_values.md +++ /dev/null @@ -1,350 +0,0 @@ ---- -title: 'Initial Attribute Values' -comment: 'Generated by the DocumentationDumperCommand, do not modify' ---- - -## 1: Check of integration "metadata-read" -Extracting the `ExifTool:ExifToolVersion` metadata allows to ensure that the metadata-read. integration is functional. The metadata `ExifTool:ExifToolVersion` in always returned by exiftool. -### Attribute(s) definition(s) ... -- __exiftoolVersion__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "metadata", - "value": "ExifTool:ExifToolVersion" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| ExifTool:ExifToolVersion | `12.42` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | exiftoolVersion | `12.42` - - ---- -## 2: Multi-values metadata -> mono-value attribute ("metadata" method) -Values will be separated by " ; " -### Attribute(s) definition(s) ... -- __Keywords__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "metadata", - "value": "IPTC:Keywords" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| IPTC:Keywords | `dog` ; `cat` ; `bird` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | Keywords | `dog ; cat ; bird` - - ---- -## 3: Multi-values metadata -> multi-values attribute ("metadata" method) -### Attribute(s) definition(s) ... -- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` - -```json -{ - "type": "metadata", - "value": "IPTC:Keywords" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| IPTC:Keywords | `dog` ; `cat` ; `bird` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | Keywords #1 | `dog` | - | Keywords #2 | `cat` | - | Keywords #3 | `bird` | - - ---- -## 4: Multi-values metadata -> multi-values attribute ("template" method) -__warning__ : __Wrong__ usage of `getMetadata(...).value` (without "s") -### Attribute(s) definition(s) ... -- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ file.getMetadata('IPTC:Keywords').value }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| IPTC:Keywords | `dog` ; `cat` ; `bird` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | Keywords | `dog ; cat ; bird` - - ---- -## 5: Multi-values metadata -> multi-values attribute ("template" method) -Good usage of `getMetadata(...).values` -### Attribute(s) definition(s) ... -- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ file.getMetadata('IPTC:Keywords').values | join('\n') }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| IPTC:Keywords | `dog` ; `cat` ; `bird` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | Keywords #1 | `dog` | - | Keywords #2 | `cat` | - | Keywords #3 | `bird` | - - ---- -## 6: Mono-value multilocale -### Attribute(s) definition(s) ... -- __Copyright__: `text` ; [ ] `multi` ; [ ] `translatable` - - - locale `en` - ```json - { - "type": "template", - "value": "(c) {{ file.getMetadata('XMP-dc:Creator').value }}. All rights reserved" - } - ``` - - locale `fr` - ```json - { - "type": "template", - "value": "(c) {{ file.getMetadata('XMP-dc:Creator').value }}. Tous droits réservés" - } - ``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| XMP-dc:Creator | `Bob` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | __locale `en`__ | | - | Copyright | `(c) Bob. All rights reserved` | - | __locale `fr`__ | | - | Copyright | `(c) Bob. Tous droits réservés` | - - ---- -## 7: Multi-values multilocale -### Attribute(s) definition(s) ... -- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` - - - locale `en` - ```json - { - "type": "metadata", - "value": "XMP-dc:Subject" - } - ``` - - locale `fr` - ```json - { - "type": "metadata", - "value": "IPTC:SupplementalCategories" - } - ``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| XMP-dc:Subject | `dog` ; `cat` ; `bird` | -| IPTC:SupplementalCategories | `chien` ; `chat` ; `oiseau` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | __locale `en`__ | | - | Keywords #1 | `dog` | - | Keywords #2 | `cat` | - | Keywords #3 | `bird` | - | __locale `fr`__ | | - | Keywords #1 | `chien` | - | Keywords #2 | `chat` | - | Keywords #3 | `oiseau` | - - ---- -## 8: Unknown tag -### Attribute(s) definition(s) ... -- __zAttribute__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ (file.getMetadata('badTag').value ?? 'whaat ?') }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | zAttribute | `whaat ?` - - ---- -## 9: First metadata set, with alternate value (1) -for this test, only the metadata `IPTC:City` is set. -### Attribute(s) definition(s) ... -- __City__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ file.getMetadata('XMP-iptcCore:CreatorCity').value ?? file.getMetadata('IPTC:City').value ?? 'no-city ?' }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| IPTC:City | `Paris` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | City | `Paris` - - ---- -## 10: First metadata set, with alternate value (2) -for this test, no metadata is set, so the default value is used. -### Attribute(s) definition(s) ... -- __City__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ file.getMetadata('XMP-iptcCore:CreatorCity').value ?? file.getMetadata('IPTC:City').value ?? 'no-city ?' }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | City | `no-city ?` - - ---- -## 11: Many metadata to a mono-value attribute -Here an array `[a, b]` is used, the `join` filter inserts a " - " only if required. -### Attribute(s) definition(s) ... -- __CreatorLocation__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ [file.getMetadata('XMP-iptcCore:CreatorCity').value, file.getMetadata('XMP-iptcCore:CreatorCountry').value] | join(' - ') }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| XMP-iptcCore:CreatorCity | `Paris` | -| XMP-iptcCore:CreatorCountry | `France` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | CreatorLocation | `Paris - France` - - ---- -## 12: Many metadata to a multi-values attribute -_nb_: We fill an array that will be joined by `\n` to generate __one line per item__ (required). - - The `merge` filter __requires__ arrays, so `IPTC:Keywords` defaults to `[]` in case there is no keywords in metadata. - -### Attribute(s) definition(s) ... -- __Keywords__: `text` ; [X] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ ( (file.getMetadata('IPTC:Keywords').values ?? []) | merge([file.getMetadata('IPTC:City').value, file.getMetadata('XMP-photoshop:Country').value ]) ) | join('\n') }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| IPTC:Keywords | `Eiffel Tower` ; `Seine river` | -| IPTC:City | `Paris` | -| XMP-photoshop:Country | `France` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | Keywords #1 | `Eiffel Tower` | - | Keywords #2 | `Seine river` | - | Keywords #3 | `Paris` | - | Keywords #4 | `France` | - - ---- -## 13: Transform code to human readable text -here the `ExposureProgram` metadata is a code (0...9), used to index an array of strings. -### Attribute(s) definition(s) ... -- __ExposureProgram__: `text` ; [ ] `multi` ; [ ] `translatable` - -```json -{ - "type": "template", - "value": "{{ [ 'Not Defined', 'Manual', 'Program AE', 'Aperture-priority AE', 'Shutter speed priority AE', 'Creative (Slow speed)', 'Action (High speed)', 'Portrait', 'Landscape', 'Bulb' ][file.getMetadata('ExifIFD:ExposureProgram').value] ?? 'Unknown mode' }}" -} -``` - -### ... with file metadata ... -| metadata | value(s) | -|---|---| -| ExifIFD:ExposureProgram | `2` | - -### ... set attribute(s) initial value(s) - | Attributes | initial value(s) | - |---|---| - | ExposureProgram | `Program AE` - - diff --git a/doc/Databox/Renditions/rendition_factory.md b/doc/Databox/Renditions/rendition_factory.md deleted file mode 100644 index 5b007ad9e..000000000 --- a/doc/Databox/Renditions/rendition_factory.md +++ /dev/null @@ -1,531 +0,0 @@ ---- -title: 'Rendition Factory' -comment: 'Generated by the DocumentationDumperCommand, do not modify' ---- - -## `imagine` transformer module -Transform an image with some filter. -```yaml -- - module: imagine # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # Output image format - format: ~ # Example: jpeg - # Filters to apply to the image - filters: - # Filter performs sizing transformations (specifically relative resizing) - relative_resize: - heighten: ~ # Example: 'value "60" => given 50x40px, output 75x60px using "heighten" option' - widen: ~ # Example: 'value "32" => given 50x40px, output 32x26px using "widen" option' - increase: ~ # Example: 'value "10" => given 50x40px, output 60x50px, using "increase" option' - scale: ~ # Example: 'value "2.5" => given 50x40px, output 125x100px using "scale" option' - # use and setup the resize filter - resize: - # set the size of the resizing area [width,height] - size: - 0: ~ # Example: '120' - 1: ~ # Example: '90' - # Filter performs thumbnail transformations (which includes scaling and potentially cropping operations) - thumbnail: - # set the thumbnail size to [width,height] pixels - size: - 0: ~ # Example: '32' - 1: ~ # Example: '32' - # Sets the desired resize method: "outbound" crops the image as required, while "inset" performs a non-cropping relative resize. - mode: ~ # Example: inset - # Toggles allowing image up-scaling when the image is smaller than the desired thumbnail size. Value: true or false - allow_upscale: ~ - # filter performs sizing transformations (which includes cropping operations) - crop: - # set the size of the cropping area [width,height] - size: - 0: ~ # Example: '300' - 1: ~ # Example: '600' - # Sets the top, left-post anchor coordinates where the crop operation starts[x, y] - start: - 0: ~ # Example: '32' - 1: ~ # Example: '160' - # filter adds a watermark to an existing image - watermark: - # Path to the watermark image - image: ~ - # filter fill background color - background_fill: - # Sets the background color HEX value. The default color is white (#fff). - color: ~ - # Sets the background opacity. The value should be within a range of 0 (fully transparent) - 100 (opaque). default opacity 100 - opacity: ~ - # filter performs file transformations (which includes metadata removal) - strip: ~ - # filter performs sizing transformations (specifically image scaling) - scale: - # Sets the "desired dimensions" [width, height], from which a relative resize is performed within these constraints. - dim: - 0: ~ # Example: '800' - 1: ~ # Example: '1000' - # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. - to: ~ # Example: '1.5' - # filter performs sizing transformations (specifically image up-scaling) - upscale: - # Sets the "desired min dimensions" [width, height], from which an up-scale is performed to meet the passed constraints. - min: - 0: ~ # Example: '1200' - 1: ~ # Example: '800' - # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. - by: ~ # Example: '0.7' - # filter performs sizing transformations (specifically image down-scaling) - downscale: - # Sets the "desired max dimensions" [width, height], from which a down-scale is performed to meet the passed constraints - max: - 0: ~ # Example: '1980' - 1: ~ # Example: '1280' - # Sets the "ratio multiple" which initiates a proportional scale operation computed by multiplying all image sides by this value. - by: ~ # Example: '0.6' - # filter performs orientation transformations (which includes rotating the image) - auto_rotate: ~ - # filter performs orientation transformations (specifically image rotation) - rotate: - # Sets the rotation angle in degrees. The default value is 0. - angle: ~ # Example: '90' - # filter performs orientation transformations (specifically image flipping) - flip: - # Sets the "flip axis" that defines the axis on which to flip the image. Valid values: x, horizontal, y, vertical - axis: ~ # Example: x - # filter performs file transformations (which includes modifying the encoding method) - interlace: - # Sets the interlace mode to encode the file with. Valid values: none, line, plane, and partition. - mode: ~ # Example: line - # filter provides a resampling transformation by allows you to change the resolution of an image - resample: - # Sets the unit to use for pixel density, either "pixels per inch" or "pixels per centimeter". Valid values: ppi and ppc - unit: ~ # Example: ppi - # Sets the horizontal resolution in the specified unit - x: ~ # Example: '300' - # Sets the vertical resolution in the specified unit - y: ~ # Example: '200' - # Sets the optional temporary work directory. This filter requires a temporary location to save out and read back in the image binary, as these operations are requires to resample an image. By default, it is set to the value of the sys_get_temp_dir() function - tmp_dir: ~ # Example: /my/custom/temporary/directory/path - # filter performs thumbnail transformations (which includes scaling and potentially cropping operations) - fixed: - # Sets the "desired width" which initiates a proportional scale operation that up- or down-scales until the image width matches this value. - width: ~ # Example: '120' - # Sets the "desired height" which initiates a proportional scale operation that up- or down-scales until the image height matches this value - height: ~ # Example: '90' - # use stamp - stamp: - # path to the font file ttf - font: ~ - # available position value: topleft, top, topright, left, center, right, bottomleft, bottom, bottomright, under, above - position: ~ - angle: 0 - # font size - size: 16 - color: '#000000' - # the font alpha value - alpha: 100 - # text to stamp - text: ~ - # text width - width: ~ - # text background, option use for position under or above - background: '#FFFFFF' - # background transparancy, option use for position under or above - transparency: null -``` -## `void` transformer module -A module that does nothing (testing purpose) -```yaml -- - module: void # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true -``` -## `video_summary` transformer module -Assemble multiple extracts (clips) of the video. -```yaml -- - module: video_summary # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # Skip video start, in seconds or timecode - start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' - # Extract one video clip every period, in seconds or timecode - period: ~ # Required, Example: '5 ; "00:00:05.00"' - # Duration of each clip, in seconds or timecode - duration: ~ # Required, Example: '0.25 ; "00:00:00.25"' - # output format - format: ~ # Required, Example: video-mpeg - # extension of the output file - extension: 'default extension from format' # Example: mpeg - # Change the number of ffmpeg passes - passes: 2 - # Change the default timeout used by ffmpeg (defaults to symphony process timeout) - timeout: ~ - # Change the default number of threads used by ffmpeg - threads: ~ -``` -### Supported output `format`s. -| Family | Format | Mime type | Extensions | -|-|-|-|-| -| video |||| -|| video-mkv | video/x-matroska | mkv | -|| video-mpeg4 | video/mp4 | mp4 | -|| video-mpeg | video/mpeg | mpeg | -|| video-quicktime | video/quicktime | mov | -|| video-webm | video/webm | webm | -|| video-ogg | video/ogg | ogv | - -## `ffmpeg` transformer module -apply filters to a video using FFMpeg. -```yaml -- - module: ffmpeg # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # output format - format: ~ - # extension of the output file - extension: ~ - # Change the default video codec used by the output format - video_codec: ~ - # Change the default audio codec used by the output format - audio_codec: ~ - # Change the default video_kilobitrate used by the output format - video_kilobitrate: ~ - # Change the default audio_kilobitrate used by the output format - audio_kilobitrate: ~ - # Change the number of ffmpeg passes - passes: 2 - # Change the default timeout used by ffmpeg (defaults to symphony process timeout) - timeout: ~ - # Change the default number of threads used by ffmpeg - threads: ~ - # Filters to apply to the video - filters: - # Prototype: see list of available filters below - - - # Name of the filter - name: ~ # Required - # Whether to enable the filter - enabled: true -``` -### Supported output `format`s. -| Family | Format | Mime type | Extensions | -|-|-|-|-| -| audio |||| -|| audio-wav | audio/wav | wav | -|| audio-aac | audio/aac | aac, m4a | -|| audio-mp3 | audio/mp3 | mp3 | -|| audio-ogg | audio/ogg | oga, ogg | -| image |||| -|| image-jpeg | image/jpeg | jpg, jpeg | -|| image-gif | image/gif | gif | -|| image-png | image/png | png | -|| image-tiff | image/tiff | tif, tiff | -| video |||| -|| video-mkv | video/x-matroska | mkv | -|| video-mpeg4 | video/mp4 | mp4 | -|| video-mpeg | video/mpeg | mpeg | -|| video-quicktime | video/quicktime | mov | -|| video-webm | video/webm | webm | -|| video-ogg | video/ogg | ogv | -### List of `ffmpeg` filters: -- `pre_clip` filter -```yaml -# Clip the video before applying other filters -- - name: pre_clip # Required - enabled: true - # Offset of frame in seconds or timecode - start: 0 # Example: '2.5 ; "00:00:02.500" ; "{{ attr.start }}"' - # Duration in seconds or timecode - duration: null # Example: '30 ; "00:00:30" ; "{{ input.duration/2 }}"' -``` -- `clip` filter -```yaml -# Clip the video or audio -- - name: clip # Required - enabled: true - # Offset of frame in seconds or timecode - start: 0 # Example: '2.5 ; "00:00:02.500" ; "{{ attr.start }}"' - # Duration in seconds or timecode - duration: null # Example: '30 ; "00:00:30" ; "{{ input.duration/2 }}"' -``` -- `remove_audio` filter -```yaml -# Remove the audio from the video -- - name: remove_audio # Required - enabled: true -``` -- `resample_audio` filter -```yaml -# Resample the audio -- - name: resample_audio # Required - enabled: true - rate: '44100' # Required -``` -- `resize` filter -```yaml -# Resize the video -- - name: resize # Required - enabled: true - # Width of the video - width: ~ # Required - # Height of the video - height: ~ # Required - # Resize mode - mode: inset # Example: inset - # Correct the width/height to the closest "standard" size - force_standards: true -``` -- `rotate` filter -```yaml -# Rotate the video -- - name: rotate # Required - enabled: true - # Angle of rotation [0 | 90 | 180 | 270] - angle: ~ # Required, Example: '90' -``` -- `pad` filter -```yaml -# Pad the video -- - name: pad # Required - enabled: true - # Width of the video - width: ~ # Required - # Height of the video - height: ~ # Required -``` -- `crop` filter -```yaml -# Crop the video -- - name: crop # Required - enabled: true - # X coordinate - x: ~ # Required - # Y coordinate - y: ~ # Required - # Width of the video - width: ~ # Required - # Height of the video - height: ~ # Required -``` -- `watermark` filter -```yaml -# Apply a watermark on the video -- - name: watermark # Required - enabled: true - # "relative" or "absolute" position - position: ~ # Required - # Path to the watermark image - path: ~ # Required - # top coordinate (only if position is "relative", set top OR bottom) - top: ~ - # bottom coordinate (only if position is "relative", set top OR bottom) - bottom: ~ - # left coordinate (only if position is "relative", set left OR right) - left: ~ - # right coordinate (only if position is "relative", set left OR right) - right: ~ - # X coordinate (only if position is "absolute") - x: ~ - # Y coordinate (only if position is "absolute") - y: ~ -``` -- `framerate` filter -```yaml -# Change the framerate -- - name: framerate # Required - enabled: true - # framerate - framerate: ~ # Required - # gop - gop: ~ -``` -- `synchronize` filter -```yaml -# re-synchronize audio and video -- - name: synchronize # Required - enabled: true -``` -## `video_to_frame` transformer module -Extract one frame from the video. -```yaml -- - module: video_to_frame # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # Offset of frame in seconds or timecode - start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' - # output format - format: ~ # Required, Example: image-jpeg - # extension of the output file - extension: 'default extension from format' # Example: jpg - # Change the quality of the output file (0-100) - quality: 80 - # Change the number of ffmpeg passes - passes: 2 - # Change the default timeout used by ffmpeg (defaults to symphony process timeout) - timeout: ~ - # Change the default number of threads used by ffmpeg - threads: ~ -``` -### Supported output `format`s. -| Family | Format | Mime type | Extensions | -|-|-|-|-| -| image |||| -|| image-jpeg | image/jpeg | jpg, jpeg | -|| image-gif | image/gif | gif | -|| image-png | image/png | png | -|| image-tiff | image/tiff | tif, tiff | - -## `video_to_animation` transformer module -Converts a video to an animated GIF / PNG. -```yaml -- - module: video_to_animation # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # Start time in seconds or timecode - start: 0 # Example: '2.5 ; "00:00:02.50" ; "{{ attr.start }}"' - # Duration in seconds or timecode - duration: null # Example: '30 ; "00:00:30.00" ; "{{ input.duration/2 }}"' - # Frames per second - fps: 1 - # Width in pixels - width: null - # Height in pixels - height: null - # Resize mode - mode: inset # One of "inset" - # output format - format: ~ # Required, Example: animated-png - # extension of the output file - extension: 'default extension from format' # Example: apng - # Change the number of ffmpeg passes - passes: 2 - # Change the default timeout used by ffmpeg (defaults to symphony process timeout) - timeout: ~ - # Change the default number of threads used by ffmpeg - threads: ~ -``` -### Supported output `format`s. -| Family | Format | Mime type | Extensions | -|-|-|-|-| -| animation |||| -|| animated-gif | image/gif | gif | -|| animated-png | image/apng | apng, png | -|| animated-webp | image/webp | webp | - -## `album_artwork` transformer module -Extract the artwork (cover) of an audio file. -```yaml -- - module: album_artwork # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # output format - format: ~ # Required, Example: image-jpeg - # extension of the output file - extension: 'default extension from format' # Example: jpg -``` -### Supported output `format`s. -| Family | Format | Mime type | Extensions | -|-|-|-|-| -| image |||| -|| image-jpeg | image/jpeg | jpg, jpeg | -|| image-gif | image/gif | gif | -|| image-png | image/png | png | -|| image-tiff | image/tiff | tif, tiff | - -## `document_to_pdf` transformer module -Convert any document to PDF format. -```yaml -- - module: document_to_pdf # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: [] -``` -## `pdf_to_image` transformer module -Convert the first page of a PDF to an image. -```yaml -- - module: pdf_to_image # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # Output image extension: jpg, jpeg, png, or webp - extension: jpeg - # Resolution of the output image in dpi - resolution: 300 - # Quality of the output image, from 0 to 100 - quality: 100 - # Size of the output image, [width, height] in pixels - size: - # Width of the output image in pixels - 0: ~ # Example: '150' - # Height of the output image in pixels - 1: ~ # Example: '100' -``` -## `set_dpi` transformer module -Change the dpi metadata of an image (no resampling). -```yaml -- - module: set_dpi # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - dpi: ~ # Required, Example: '72' -``` -## `download` transformer module -Download a file to be used as output. -```yaml -- - module: download # Required - # Description of the module action - description: ~ - # Whether to enable this module - enabled: true - options: - # url of the file to download - url: ~ -``` - From 12bd7f43f7f242e193a176330196b60d148e7d21 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Mon, 28 Jul 2025 18:21:03 +0200 Subject: [PATCH 28/28] generates databox-api schema (json) --- .../ApiDocumentationGenerator.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 databox/api/src/Documentation/ApiDocumentationGenerator.php diff --git a/databox/api/src/Documentation/ApiDocumentationGenerator.php b/databox/api/src/Documentation/ApiDocumentationGenerator.php new file mode 100644 index 000000000..8daf298ad --- /dev/null +++ b/databox/api/src/Documentation/ApiDocumentationGenerator.php @@ -0,0 +1,42 @@ +application = new Application($kernel); + $this->application->setAutoExit(false); + } + + public function getPath(): string + { + return 'Databox/Api/schema.json'; + } + + public function getTitle(): string + { + return 'Api Schema'; + } + + public function getContent(): string + { + $input = new ArrayInput([ + 'command' => 'api:openapi:export', + ]); + $output = new BufferedOutput(); + $this->application->run($input, $output); + + return $output->fetch(); + } +}