From 292d90748697d1c9866eba88facd34bd9e731d4e Mon Sep 17 00:00:00 2001 From: BowlOfSoup Date: Sat, 29 Nov 2025 12:26:25 +0100 Subject: [PATCH] ISSUE-75: First version of CLAUDE + Dagger --- .dagger/.gitignore | 9 ++ .dagger/src/NormalizerBundle.php | 187 +++++++++++++++++++++++++++++ .github/workflows/ci.yaml | 57 ++++----- .gitignore | 5 +- CLAUDE.md | 194 +++++++++++++++++++++++++++++++ DAGGER.md | 162 ++++++++++++++++++++++++++ dagger-ci.sh | 84 +++++++++++++ dagger.json | 8 ++ 8 files changed, 673 insertions(+), 33 deletions(-) create mode 100644 .dagger/.gitignore create mode 100644 .dagger/src/NormalizerBundle.php create mode 100644 CLAUDE.md create mode 100644 DAGGER.md create mode 100755 dagger-ci.sh create mode 100644 dagger.json diff --git a/.dagger/.gitignore b/.dagger/.gitignore new file mode 100644 index 0000000..29c9604 --- /dev/null +++ b/.dagger/.gitignore @@ -0,0 +1,9 @@ +# Auto-generated by Dagger - do not commit +/sdk +/vendor +/.env +composer.json +composer.lock +entrypoint.php +README.md +.gitattributes diff --git a/.dagger/src/NormalizerBundle.php b/.dagger/src/NormalizerBundle.php new file mode 100644 index 0000000..e3a614a --- /dev/null +++ b/.dagger/src/NormalizerBundle.php @@ -0,0 +1,187 @@ +container() + ->from("php:{$phpVersion}-cli") + ->withExec(['apt-get', 'update']) + ->withExec([ + 'apt-get', + 'install', + '-y', + 'git', + 'unzip', + 'libzip-dev', + 'libxml2-dev', + ]) + ->withExec(['docker-php-ext-install', 'zip', 'dom', 'simplexml']) + ->withExec([ + 'sh', + '-c', + 'curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --version=1.10.22', + ]) + ->withExec(['sh', '-c', "echo 'memory_limit = -1' > /usr/local/etc/php/conf.d/memory.ini"]) + ->withEnvVariable('COMPOSER_MEMORY_LIMIT', '-1') + ->withMountedDirectory('/src', $source) + ->withWorkdir('/src') + ->withExec(['composer', 'install', '--no-interaction', '--prefer-dist']); + } + + #[DaggerFunction] + #[Doc('Run Rector checks (dry-run)')] + public function rector( + #[Doc('The source directory')] + Directory $source, + #[Doc('PHP version to use')] + string $phpVersion = '7.4' + ): string { + return $this->base($source, $phpVersion) + ->withExec([ + 'vendor/bin/rector', + 'process', + '--dry-run', + '--no-progress-bar', + '--ansi', + ]) + ->stdout(); + } + + #[DaggerFunction] + #[Doc('Run PHPStan static analysis')] + public function phpstan( + #[Doc('The source directory')] + Directory $source, + #[Doc('PHP version to use')] + string $phpVersion = '7.4' + ): string { + return $this->base($source, $phpVersion) + ->withExec(['vendor/bin/phpstan', 'analyze', '--no-progress', '--ansi']) + ->stdout(); + } + + #[DaggerFunction] + #[Doc('Run PHP-CS-Fixer checks (dry-run)')] + public function phpCsFixer( + #[Doc('The source directory')] + Directory $source, + #[Doc('PHP version to use')] + string $phpVersion = '7.4' + ): string { + return $this->base($source, $phpVersion) + ->withExec(['vendor/bin/php-cs-fixer', 'fix', '--dry-run', '--diff']) + ->stdout(); + } + + #[DaggerFunction] + #[Doc('Run PHPUnit tests')] + public function phpunit( + #[Doc('The source directory')] + Directory $source, + #[Doc('PHP version to use')] + string $phpVersion = '7.4' + ): string { + return $this->base($source, $phpVersion) + ->withExec(['vendor/bin/phpunit']) + ->stdout(); + } + + #[DaggerFunction] + #[Doc('Run PHPUnit tests with coverage and export coverage directory')] + public function phpunitCoverage( + #[Doc('The source directory')] + Directory $source, + #[Doc('PHP version to use')] + string $phpVersion = '8.2' + ): Directory { + $container = $this->base($source, $phpVersion) + ->withExec(['pecl', 'install', 'xdebug']) + ->withExec(['docker-php-ext-enable', 'xdebug']) + ->withEnvVariable('XDEBUG_MODE', 'coverage') + ->withExec(['php', 'vendor/bin/phpunit']); + + return $container->directory('/src/tests/coverage'); + } + + #[DaggerFunction] + #[Doc('Run all CI checks (PHPStan and PHPUnit)')] + public function test( + #[Doc('The source directory')] + Directory $source, + #[Doc('PHP version to use')] + string $phpVersion = '7.4', + #[Doc('Include Rector checks (optional, disabled by default)')] + bool $includeRector = false + ): string { + $container = $this->base($source, $phpVersion); + + // Run Rector if requested + if ($includeRector) { + $rectorOutput = $container + ->withExec([ + 'vendor/bin/rector', + 'process', + '--dry-run', + '--no-progress-bar', + '--ansi', + ]) + ->stdout(); + } + + // Run PHPStan + $phpstanOutput = $container + ->withExec(['vendor/bin/phpstan', 'analyze', '--no-progress', '--ansi']) + ->stdout(); + + // Run PHPUnit and return output + $phpunitOutput = $container + ->withExec(['vendor/bin/phpunit']) + ->stdout(); + + return "✅ All CI checks passed!\n\n" . $phpunitOutput; + } + + #[DaggerFunction] + #[Doc('Run CI for multiple PHP versions')] + public function testMatrix( + #[Doc('The source directory')] + Directory $source + ): string { + $versions = ['7.2', '7.4', '8.2']; + $results = []; + + foreach ($versions as $version) { + try { + $this->test($source, $version); + $results[] = "✅ PHP {$version}: PASSED"; + } catch (\Exception $e) { + $results[] = "❌ PHP {$version}: FAILED"; + throw $e; + } + } + + return implode("\n", $results); + } +} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1ea8d56..518987d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,47 +11,40 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: [7.2, 7.4, 8.2] - include: - - php-version: 8.2 - env: - SYMFONY_VERSION: ~5.4 + php-version: [7.4, 8.2] fail-fast: false steps: - uses: actions/checkout@v3 - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-version }} - tools: composer:1.10.22 - - - name: Cache Composer dependencies - uses: actions/cache@v2 - with: - path: $HOME/.composer/cache - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ runner.os }}-composer- - - - name: Install dependencies + - name: Install Dagger CLI run: | - if [ "${{ matrix.env.SYMFONY_VERSION }}" != "" ]; then composer require "symfony/symfony:${{ matrix.env.SYMFONY_VERSION }}" --no-update; fi; - COMPOSER_MEMORY_LIMIT=-1 composer install + curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh + echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: Setup Codecov - run: | - curl -Os https://uploader.codecov.io/latest/linux/codecov - chmod +x codecov + - name: Run Dagger CI Pipeline + run: dagger call test --source=. --php-version=${{ matrix.php-version }} + env: + DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }} - - name: Run Rector - run: vendor/bin/rector process --dry-run --no-progress-bar --ansi + # Run with code coverage on PHP 8.2 and upload to Codecov + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 - - name: Run PHPStan - run: vendor/bin/phpstan analyze --no-progress --ansi + - name: Install Dagger CLI + run: | + curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh + echo "$HOME/.local/bin" >> $GITHUB_PATH - - name: Run PHPUnit tests - run: XDEBUG_MODE=coverage php vendor/bin/phpunit + - name: Run PHPUnit with Coverage + run: | + dagger call phpunit-coverage --source=. --php-version=8.2 export --path=./coverage - name: Upload coverage to Codecov - run: ./codecov \ No newline at end of file + uses: codecov/codecov-action@v4 + with: + files: ./coverage/clover.xml + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 45bdb03..21ee5be 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ !/var/**/ !/var/**/.gitkeep /phpstan.neon ->>>>>>> fc967e0fc51238085fa8a2a2882bcdab42090dec + +# Dagger +/.dagger/vendor/ +/.dagger/sdk/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e3f4330 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,194 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Bowl Of Soup Normalizer is a Symfony bundle that provides annotation-based normalization and serialization of objects. It uses an opt-in mechanism where properties/methods must be explicitly marked for normalization using annotations. + +**Key Features:** +- Normalizes class properties and methods (public, protected, private) via annotations +- Handles Doctrine proxy objects and circular references +- Object caching via `getId()` method to avoid re-normalizing same objects +- Annotation caching (per normalize command and permanent in prod mode) +- Supports Symfony translations with locale and domain configuration +- Context groups for different normalization scenarios + +## Development Commands + +**See [DAGGER.md](DAGGER.md) for the recommended way to run tests and CI checks using Dagger.** + +### Quick Start with Dagger + +```bash +# Run all CI checks (PHPStan + PHPUnit) +./dagger-ci.sh + +# Run with specific PHP version +./dagger-ci.sh test 8.2 + +# Generate coverage report +./dagger-ci.sh coverage + +# Run individual checks +./dagger-ci.sh phpunit 7.4 +./dagger-ci.sh phpstan 8.2 +``` + +### Running Tests Directly (without Dagger) + +```bash +# Run all tests +vendor/bin/phpunit + +# Run tests with coverage (requires Xdebug) +XDEBUG_MODE=coverage php -dzend_extension=xdebug.so vendor/bin/phpunit +# Coverage output: tests/coverage/ + +# Run specific test file +vendor/bin/phpunit tests/Service/NormalizerTest.php +``` + +### Code Quality Tools (Direct) + +```bash +# Static analysis (level 3) +vendor/bin/phpstan + +# Code style fixing +vendor/bin/php-cs-fixer fix + +# Automated refactoring (dry-run recommended first) +vendor/bin/rector process --dry-run --no-progress-bar --ansi + +# Apply rector changes +vendor/bin/rector process +``` + +**Note:** Using Dagger (via `./dagger-ci.sh`) is recommended as it ensures consistent environments and matches the CI pipeline exactly. + +## Architecture + +### Core Components + +**Normalizer** (`src/Service/Normalizer.php`) +- Entry point for normalization operations +- Handles both single objects and collections +- Manages ObjectCache for circular reference detection +- Delegates to PropertyNormalizer and MethodNormalizer + +**Serializer** (`src/Service/Serializer.php`) +- Wraps Normalizer with encoding capabilities (JSON, XML) +- Uses EncoderFactory to create encoders +- Supports sorting via `@Serialize` annotation + +**ObjectCache** (`src/Model/ObjectCache.php`) +- Static cache preventing circular references +- Caches normalized results by object name and identifier (from `getId()`) +- Must be cleared between normalize operations + +### Annotation System + +Three main annotations in `src/Annotation/`: + +**@Normalize** - Properties/methods to include in normalization +- `name`: Output key name +- `group`: Array of context groups +- `type`: Special handling (collection, datetime, object) +- `format`: Date format for datetime types +- `callback`: Method to call for value transformation +- `normalizeCallbackResult`: Whether to normalize callback return value +- `skipEmpty`: Skip if value is empty +- `maxDepth`: Limit object nesting depth + +**@Serialize** - Class-level serialization configuration +- `sortProperties`: Sort output keys alphabetically +- `group`: Context group for serialization + +**@Translate** - Translate values using Symfony translator +- `locale`: Translation locale +- `domain`: Translation domain (filename) + +### Extractors + +Located in `src/Service/Extractor/`: +- `AnnotationExtractor`: Parses annotations from classes/properties/methods +- `PropertyExtractor`: Extracts property metadata +- `MethodExtractor`: Extracts method metadata +- `ClassExtractor`: Coordinates extraction process + +### Normalizers + +Located in `src/Service/Normalize/`: +- `PropertyNormalizer`: Normalizes object properties +- `MethodNormalizer`: Normalizes method return values +- Both extend `AbstractNormalizer` which handles type-specific normalization logic + +### Encoders + +Located in `src/Service/Encoder/`: +- `EncoderJson`: JSON encoding +- `EncoderXml`: XML encoding +- `EncoderFactory`: Creates encoder instances by type string + +## Important Patterns + +### Context Groups +Annotations use `group` parameter to control normalization context: +```php +@Normalize(name="email", group={"api", "admin"}) +@Normalize(name="internalId", group={"admin"}) +``` +When normalizing, pass group to include only matching annotations. + +### Circular Reference Handling +Objects implementing `getId()` are cached and reused. If a circular reference is detected, the object's ID value is returned instead of re-normalizing. + +### Doctrine Proxy Support +The bundle handles Doctrine proxy objects by extracting the real class name before processing. + +### Additional PHP Coding Standards + +#### General PHP standards + +- `declare(strict_types=1);` MUST be declared at the top for *new* PHP files +- Short array notation MUST be used +- Use Monolog for all logging operations (Psr\Log\LoggerInterface) +- Declare statements MUST be terminated by a semicolon +- Classes from the global namespace MUST NOT be imported (but prefixed with \) +- Import statements MUST be alphabetized +- The PHP features "parameter type widening" and "contravariant argument types" MUST NOT be used +- Boolean operators between conditions MUST always be at the beginning of the line +- The concatenation operator MUST be preceded and followed by one space +- Do not use YODA conditions +- All boolean API property names MUST be prepended with "is", "has" or "should" + +#### PHPDoc + +- There MUST NOT be an @author, @version or @copyright tag +- Extended type information SHOULD be used when possible (PHPStan array types) +- Arguments MUST be documented as relaxed as possible, while return values MUST be documented as precise as possible +- The @param tag MUST be omitted when the argument is properly type-hinted +- The @return tag MUST be omitted when the method or function has a proper return type-hint +- An entire docblock MUST be omitted in case it does not add "any value" over the method name, argument types and return type +- Constants MUST NOT be documented using the @var tag + +#### PHPUnit + +- We use Mockery for mocking, so all tests that use Mockery MUST extend Mockery\Adapter\Phpunit\MockeryTestCase +- Unit tests MUST not use the `@testdox` tag, but use a descriptive human-readable test method name instead +- Data providers MUST be used when possible and applicable +- All unit test class members MUST be unset in the `tearDown()` method + +## Configuration Files + +- `phpunit.xml`: Test configuration, excludes DI/EventListener/Exception/Model from coverage +- `.php-cs-fixer.php`: Custom finder supporting Git diff, STDIN, or CLI input; PSR-2/Symfony standards +- `rector.php`: Configured for PHP 7.2+ and Symfony 5.4 +- `phpstan.neon.dist`: Level 3 analysis, Symfony extension enabled + +## Testing + +Tests use fixtures in `tests/assets/` directory (Person, Address, Social, etc.). The `NormalizerTestTrait` provides common test setup for service instantiation. + +Test structure mirrors source: `tests/Service/`, `tests/Annotation/`, etc. \ No newline at end of file diff --git a/DAGGER.md b/DAGGER.md new file mode 100644 index 0000000..20d25ba --- /dev/null +++ b/DAGGER.md @@ -0,0 +1,162 @@ +# Dagger CI Pipeline + +This project uses [Dagger](https://dagger.io) for CI/CD, with a PHP-based pipeline that runs both locally and in GitHub Actions. + +## Quick Start + +### Run the complete CI pipeline locally + +```bash +# Using the helper script (easiest) +./dagger-ci.sh + +# Or with a specific PHP version +./dagger-ci.sh test 8.2 + +# Or using dagger directly +dagger call test --source=. --php-version=7.4 +``` + +## Available Commands + +### Helper Script (`./dagger-ci.sh`) + +```bash +./dagger-ci.sh [command] [php-version] + +Commands: + test Run all CI checks (PHPStan + PHPUnit) - default + phpunit Run only PHPUnit tests + phpstan Run only PHPStan analysis + rector Run only Rector checks + php-cs-fixer Run only PHP-CS-Fixer + coverage Generate code coverage report (HTML + Clover XML) + test-all Run tests on all PHP versions (7.2, 7.4, 8.2) + +Examples: + ./dagger-ci.sh # Run all checks with PHP 7.4 + ./dagger-ci.sh test 8.2 # Run all checks with PHP 8.2 + ./dagger-ci.sh phpunit 7.4 # Run only tests with PHP 7.4 + ./dagger-ci.sh coverage # Generate coverage (saves to tests/coverage/) + ./dagger-ci.sh test-all # Run on all PHP versions +``` + +### Direct Dagger Commands + +```bash +# Run all checks +dagger call test --source=. --php-version=7.4 + +# Run individual checks +dagger call phpunit --source=. --php-version=8.2 +dagger call phpstan --source=. --php-version=7.4 +dagger call rector --source=. --php-version=7.4 +dagger call php-cs-fixer --source=. --php-version=7.4 + +# Generate coverage and export to local directory +dagger call phpunit-coverage --source=. --php-version=8.2 export --path=./tests/coverage + +# Run on all PHP versions +dagger call test-matrix --source=. + +# List all available functions +dagger functions +``` + +## Code Coverage + +### Local Coverage Reports + +Generate coverage reports locally: + +```bash +# Using helper script (easiest) +./dagger-ci.sh coverage + +# Or using dagger directly +dagger call phpunit-coverage --source=. --php-version=8.2 export --path=./tests/coverage +``` + +This will: +- Run PHPUnit with Xdebug coverage enabled +- Generate HTML report at `tests/coverage/index.html` +- Generate Clover XML at `tests/coverage/clover.xml` + +Open the HTML report in your browser: +```bash +open tests/coverage/index.html # macOS +xdg-open tests/coverage/index.html # Linux +``` + +### GitHub Actions + Codecov + +Coverage is automatically generated and uploaded to Codecov on every push/PR: +1. Tests run with coverage on PHP 8.2 +2. Clover XML is exported +3. Uploaded to Codecov using `codecov/codecov-action@v4` + +**Required Secret**: Add `CODECOV_TOKEN` to your GitHub repository secrets. + +Get your token from: https://codecov.io/gh/BowlOfSoup/NormalizerBundle + +## Supported PHP Versions + +- PHP 7.2 +- PHP 7.4 (default) +- PHP 8.2 + +## Pipeline Configuration + +The pipeline is defined in `.dagger/src/NormalizerBundle.php` using PHP with Dagger attributes. + +### What the pipeline does + +1. **Base setup**: Creates a PHP container with all required extensions and dependencies + - Installs system packages (git, unzip, libzip-dev, libxml2-dev) + - Installs PHP extensions (zip, dom, simplexml) + - Installs Composer 1.10.22 + - Sets unlimited PHP memory for tools + - Runs `composer install` + +2. **Quality checks**: + - PHPStan: Static analysis (level 3) + - PHPUnit: Test suite (77 tests, 169 assertions) + - Rector: Automated refactoring checks (optional) + - PHP-CS-Fixer: Code style checks (optional) + +## GitHub Actions + +The pipeline runs automatically in GitHub Actions: +- On every push to `master` +- On every pull request +- Tests PHP 7.4 and 8.2 in parallel + +See `.github/workflows/ci.yaml` for the configuration. + +## Requirements + +- [Dagger CLI](https://docs.dagger.io/install) installed locally +- Docker running on your machine + +## Customizing the Pipeline + +Edit `.dagger/src/NormalizerBundle.php` to modify the pipeline. The file uses PHP attributes to define Dagger functions: + +```php +#[DaggerFunction] +#[Doc('Run PHPUnit tests')] +public function phpunit(Directory $source, string $phpVersion = '7.4'): string +{ + return $this->base($source, $phpVersion) + ->withExec(['vendor/bin/phpunit']) + ->stdout(); +} +``` + +## Benefits of Dagger + +- **Same pipeline locally and in CI**: No more "works on my machine" +- **Fast**: Docker layer caching speeds up repeated runs +- **Portable**: Works on any machine with Docker +- **Easy to modify**: Pure PHP code, no YAML configuration hell +- **Type-safe**: Full IDE support and type checking diff --git a/dagger-ci.sh b/dagger-ci.sh new file mode 100755 index 0000000..54813f2 --- /dev/null +++ b/dagger-ci.sh @@ -0,0 +1,84 @@ +#!/bin/bash +# Dagger CI Helper Script +# +# This script provides easy shortcuts to run the Dagger CI pipeline locally. +# +# Usage: +# ./dagger-ci.sh [command] [php-version] +# +# Commands: +# test - Run all CI checks (default) +# phpunit - Run only PHPUnit tests +# phpstan - Run only PHPStan analysis +# rector - Run only Rector checks +# php-cs-fixer - Run only PHP-CS-Fixer +# coverage - Run tests with coverage and export to tests/coverage/ +# test-all - Run tests on all PHP versions (7.2, 7.4, 8.2) +# +# Examples: +# ./dagger-ci.sh # Run all checks with PHP 7.4 +# ./dagger-ci.sh test 8.2 # Run all checks with PHP 8.2 +# ./dagger-ci.sh phpunit 7.4 # Run only tests with PHP 7.4 +# ./dagger-ci.sh coverage # Generate coverage report (PHP 8.2) +# ./dagger-ci.sh test-all # Run tests on all PHP versions + +set -e + +COMMAND=${1:-test} +PHP_VERSION=${2:-7.4} + +case "$COMMAND" in + test) + echo "Running all CI checks with PHP $PHP_VERSION..." + dagger call test --source=. --php-version="$PHP_VERSION" + ;; + + phpunit) + echo "Running PHPUnit tests with PHP $PHP_VERSION..." + dagger call phpunit --source=. --php-version="$PHP_VERSION" + ;; + + phpstan) + echo "Running PHPStan analysis with PHP $PHP_VERSION..." + dagger call phpstan --source=. --php-version="$PHP_VERSION" + ;; + + rector) + echo "Running Rector checks with PHP $PHP_VERSION..." + dagger call rector --source=. --php-version="$PHP_VERSION" + ;; + + php-cs-fixer) + echo "Running PHP-CS-Fixer with PHP $PHP_VERSION..." + dagger call php-cs-fixer --source=. --php-version="$PHP_VERSION" + ;; + + coverage) + echo "Running PHPUnit with coverage (PHP 8.2)..." + echo "Coverage reports will be saved to tests/coverage/" + dagger call phpunit-coverage --source=. --php-version=8.2 export --path=./tests/coverage + echo "" + echo "✅ Coverage generated!" + echo " HTML report: tests/coverage/index.html" + echo " Clover XML: tests/coverage/clover.xml" + ;; + + test-all) + echo "Running tests on all PHP versions..." + dagger call test-matrix --source=. + ;; + + *) + echo "Unknown command: $COMMAND" + echo "" + echo "Available commands:" + echo " test - Run all CI checks (default)" + echo " phpunit - Run only PHPUnit tests" + echo " phpstan - Run only PHPStan analysis" + echo " rector - Run only Rector checks" + echo " php-cs-fixer - Run only PHP-CS-Fixer" + echo " coverage - Run tests with coverage (exports to tests/coverage/)" + echo " test-all - Run tests on all PHP versions" + exit 1 + ;; +esac diff --git a/dagger.json b/dagger.json new file mode 100644 index 0000000..5231c58 --- /dev/null +++ b/dagger.json @@ -0,0 +1,8 @@ +{ + "name": "NormalizerBundle", + "engineVersion": "v0.19.7", + "sdk": { + "source": "php" + }, + "source": ".dagger" +}