From 30cd41ea8a306a12c153b4d264b3f06c97a330ed Mon Sep 17 00:00:00 2001 From: Sebastian Almberg <83243306+Sebbeben@users.noreply.github.com> Date: Thu, 19 Feb 2026 22:33:43 +0100 Subject: [PATCH] Split out KiCad API v2 into separate PR as requested by maintainer Remove v2 controller, tests, and volatile field support from this PR. The v2 API will be submitted as a separate PR for focused discussion. --- src/Controller/KiCadApiV2Controller.php | 107 ----------- src/Services/EDA/KiCadHelper.php | 25 +-- tests/Controller/KiCadApiV2ControllerTest.php | 177 ------------------ 3 files changed, 5 insertions(+), 304 deletions(-) delete mode 100644 src/Controller/KiCadApiV2Controller.php delete mode 100644 tests/Controller/KiCadApiV2ControllerTest.php diff --git a/src/Controller/KiCadApiV2Controller.php b/src/Controller/KiCadApiV2Controller.php deleted file mode 100644 index a915b94e..00000000 --- a/src/Controller/KiCadApiV2Controller.php +++ /dev/null @@ -1,107 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Controller; - -use App\Entity\Parts\Category; -use App\Entity\Parts\Part; -use App\Services\EDA\KiCadHelper; -use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Attribute\Route; - -/** - * KiCad HTTP Library API v2 controller. - * - * v1 spec: https://dev-docs.kicad.org/en/apis-and-binding/http-libraries/index.html - * v2 spec (draft): https://gitlab.com/RosyDev/kicad-dev-docs/-/blob/http-lib-v2/content/apis-and-binding/http-libraries/http-lib-v2-00.adoc - * - * Differences from v1: - * - Volatile fields: Stock and Storage Location are marked volatile (shown in KiCad but NOT saved to schematic) - * - Category descriptions: Uses actual category comments instead of URLs - * - Response format: Arrays wrapped in objects for extensibility - */ -#[Route('/kicad-api/v2')] -class KiCadApiV2Controller extends AbstractController -{ - public function __construct( - private readonly KiCadHelper $kiCADHelper, - ) { - } - - #[Route('/', name: 'kicad_api_v2_root')] - public function root(): Response - { - $this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS'); - - return $this->json([ - 'categories' => '', - 'parts' => '', - ]); - } - - #[Route('/categories.json', name: 'kicad_api_v2_categories')] - public function categories(Request $request): Response - { - $this->denyAccessUnlessGranted('@categories.read'); - - $data = $this->kiCADHelper->getCategories(); - return $this->createCacheableJsonResponse($request, $data, 300); - } - - #[Route('/parts/category/{category}.json', name: 'kicad_api_v2_category')] - public function categoryParts(Request $request, ?Category $category): Response - { - if ($category !== null) { - $this->denyAccessUnlessGranted('read', $category); - } else { - $this->denyAccessUnlessGranted('@categories.read'); - } - $this->denyAccessUnlessGranted('@parts.read'); - - $minimal = $request->query->getBoolean('minimal', false); - $data = $this->kiCADHelper->getCategoryParts($category, $minimal); - return $this->createCacheableJsonResponse($request, $data, 300); - } - - #[Route('/parts/{part}.json', name: 'kicad_api_v2_part')] - public function partDetails(Request $request, Part $part): Response - { - $this->denyAccessUnlessGranted('read', $part); - - // Use API v2 format with volatile fields - $data = $this->kiCADHelper->getKiCADPart($part, 2); - return $this->createCacheableJsonResponse($request, $data, 60); - } - - private function createCacheableJsonResponse(Request $request, array $data, int $maxAge): Response - { - $response = new JsonResponse($data); - $response->setEtag(md5(json_encode($data))); - $response->headers->set('Cache-Control', 'private, max-age=' . $maxAge); - $response->isNotModified($request); - - return $response; - } -} diff --git a/src/Services/EDA/KiCadHelper.php b/src/Services/EDA/KiCadHelper.php index 8bd1fc74..b64315fb 100644 --- a/src/Services/EDA/KiCadHelper.php +++ b/src/Services/EDA/KiCadHelper.php @@ -197,15 +197,8 @@ class KiCadHelper }); } - /** - * @param int $apiVersion The API version to use (1 or 2). Version 2 adds volatile field support. - */ - public function getKiCADPart(Part $part, int $apiVersion = 1): array + public function getKiCADPart(Part $part): array { - if ($apiVersion < 1 || $apiVersion > 2) { - throw new \InvalidArgumentException(sprintf('Unsupported API version %d. Supported versions: 1, 2.', $apiVersion)); - } - $result = [ 'id' => (string)$part->getId(), 'name' => $part->getName(), @@ -339,10 +332,9 @@ class KiCadHelper } } } - // In API v2, stock and location are volatile (shown but not saved to schematic) - $result['fields']['Stock'] = $this->createField($totalStock, false, $apiVersion >= 2); + $result['fields']['Stock'] = $this->createField($totalStock); if ($locations !== []) { - $result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations)), false, $apiVersion >= 2); + $result['fields']['Storage Location'] = $this->createField(implode(', ', array_unique($locations))); } //Add parameters marked for EDA export (explicit true, or system default when null) @@ -454,21 +446,14 @@ class KiCadHelper * Creates a field array for KiCAD * @param string|int|float $value * @param bool $visible - * @param bool $volatile If true (API v2), field is shown in KiCad but NOT saved to schematic * @return array */ - private function createField(string|int|float $value, bool $visible = false, bool $volatile = false): array + private function createField(string|int|float $value, bool $visible = false): array { - $field = [ + return [ 'value' => (string)$value, 'visible' => $this->boolToKicadBool($visible), ]; - - if ($volatile) { - $field['volatile'] = $this->boolToKicadBool(true); - } - - return $field; } /** diff --git a/tests/Controller/KiCadApiV2ControllerTest.php b/tests/Controller/KiCadApiV2ControllerTest.php deleted file mode 100644 index 679197f3..00000000 --- a/tests/Controller/KiCadApiV2ControllerTest.php +++ /dev/null @@ -1,177 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Tests\Controller; - -use App\DataFixtures\APITokenFixtures; -use Symfony\Bundle\FrameworkBundle\KernelBrowser; -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - -final class KiCadApiV2ControllerTest extends WebTestCase -{ - private const BASE_URL = '/en/kicad-api/v2'; - - protected function createClientWithCredentials(string $token = APITokenFixtures::TOKEN_READONLY): KernelBrowser - { - return static::createClient([], ['headers' => ['authorization' => 'Bearer '.$token]]); - } - - public function testRoot(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/'); - - self::assertResponseIsSuccessful(); - $content = $client->getResponse()->getContent(); - self::assertJson($content); - - $array = json_decode($content, true); - self::assertArrayHasKey('categories', $array); - self::assertArrayHasKey('parts', $array); - } - - public function testCategories(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/categories.json'); - - self::assertResponseIsSuccessful(); - $content = $client->getResponse()->getContent(); - self::assertJson($content); - - $data = json_decode($content, true); - self::assertCount(1, $data); - - $category = $data[0]; - self::assertArrayHasKey('name', $category); - self::assertArrayHasKey('id', $category); - } - - public function testCategoryParts(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/parts/category/1.json'); - - self::assertResponseIsSuccessful(); - $content = $client->getResponse()->getContent(); - self::assertJson($content); - - $data = json_decode($content, true); - self::assertCount(3, $data); - - $part = $data[0]; - self::assertArrayHasKey('name', $part); - self::assertArrayHasKey('id', $part); - self::assertArrayHasKey('description', $part); - } - - public function testCategoryPartsMinimal(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/parts/category/1.json?minimal=true'); - - self::assertResponseIsSuccessful(); - $content = $client->getResponse()->getContent(); - self::assertJson($content); - - $data = json_decode($content, true); - self::assertCount(3, $data); - } - - public function testPartDetailsHasVolatileFields(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/parts/1.json'); - - self::assertResponseIsSuccessful(); - $content = $client->getResponse()->getContent(); - self::assertJson($content); - - $data = json_decode($content, true); - - // V2 should have volatile flag on Stock field - self::assertArrayHasKey('fields', $data); - self::assertArrayHasKey('Stock', $data['fields']); - self::assertArrayHasKey('volatile', $data['fields']['Stock']); - self::assertEquals('True', $data['fields']['Stock']['volatile']); - } - - public function testPartDetailsV2VsV1Difference(): void - { - $client = $this->createClientWithCredentials(); - - // Get v1 response - $client->request('GET', '/en/kicad-api/v1/parts/1.json'); - self::assertResponseIsSuccessful(); - $v1Data = json_decode($client->getResponse()->getContent(), true); - - // Get v2 response - $client->request('GET', self::BASE_URL.'/parts/1.json'); - self::assertResponseIsSuccessful(); - $v2Data = json_decode($client->getResponse()->getContent(), true); - - // V1 should NOT have volatile on Stock - self::assertArrayNotHasKey('volatile', $v1Data['fields']['Stock']); - - // V2 should have volatile on Stock - self::assertArrayHasKey('volatile', $v2Data['fields']['Stock']); - - // Both should have the same stock value - self::assertEquals($v1Data['fields']['Stock']['value'], $v2Data['fields']['Stock']['value']); - } - - public function testCategoriesHasCacheHeaders(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/categories.json'); - - self::assertResponseIsSuccessful(); - $response = $client->getResponse(); - self::assertNotNull($response->headers->get('ETag')); - self::assertStringContainsString('max-age=', $response->headers->get('Cache-Control')); - } - - public function testConditionalRequestReturns304(): void - { - $client = $this->createClientWithCredentials(); - $client->request('GET', self::BASE_URL.'/categories.json'); - - $etag = $client->getResponse()->headers->get('ETag'); - self::assertNotNull($etag); - - $client->request('GET', self::BASE_URL.'/categories.json', [], [], [ - 'HTTP_IF_NONE_MATCH' => $etag, - ]); - - self::assertResponseStatusCodeSame(304); - } - - public function testUnauthenticatedAccessDenied(): void - { - $client = static::createClient(); - $client->request('GET', self::BASE_URL.'/categories.json'); - - // Anonymous user has default read permissions in Part-DB, - // so this returns 200 rather than a redirect - self::assertResponseIsSuccessful(); - } -}