diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml new file mode 100644 index 00000000..2605e582 --- /dev/null +++ b/.github/workflows/api-docs.yml @@ -0,0 +1,35 @@ +name: API documentation contract + +on: + pull_request: + paths: + - 'vendor/postman' + - 'scripts/**' + - 'package.json' + - 'pnpm-lock.yaml' + - '.github/workflows/api-docs.yml' + - '.github/workflows/bump-postman.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: pnpm/action-setup@v3 + with: + version: 10 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - name: Verify complete PHP catalog before postinstall + run: node scripts/sync-php-sdk-examples.mjs + - run: pnpm install --frozen-lockfile + - run: node --test scripts/sdk-emitters.test.mjs scripts/sync-php-sdk-examples.test.mjs + - run: pnpm generate:api-docs diff --git a/.github/workflows/bump-postman.yml b/.github/workflows/bump-postman.yml index 77c2c33a..ddd79395 100644 --- a/.github/workflows/bump-postman.yml +++ b/.github/workflows/bump-postman.yml @@ -12,6 +12,13 @@ on: repository_dispatch: types: [postman-updated] workflow_dispatch: + schedule: + # Retry after an SDK contract update is merged, even without a Postman push. + - cron: '17 5 * * *' + +concurrency: + group: bump-postman + cancel-in-progress: false permissions: contents: write @@ -45,48 +52,52 @@ jobs: echo "old_short=${old_sha:0:7}" >> "$GITHUB_OUTPUT" echo "new_short=${new_sha:0:7}" >> "$GITHUB_OUTPUT" - if [ "$old_sha" = "$new_sha" ]; then - echo "no_change=true" >> "$GITHUB_OUTPUT" - echo "✅ Submodule is already at latest main ($old_sha) — nothing to do." - else - echo "no_change=false" >> "$GITHUB_OUTPUT" - echo "🆕 Bumping submodule: $old_sha → $new_sha" - fi - - - name: Stop early if nothing changed - if: steps.bump.outputs.no_change == 'true' - run: exit 0 + echo "Postman: $old_sha → $new_sha; checking the PHP catalog as well." - name: Setup pnpm - if: steps.bump.outputs.no_change != 'true' uses: pnpm/action-setup@v3 with: version: 10 - name: Setup Node.js - if: steps.bump.outputs.no_change != 'true' uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm + - name: Checkout the reviewed PHP SDK catalog + uses: actions/checkout@v4 + with: + repository: fleetbase/fleetbase-php + ref: main + path: .php-sdk-source + persist-credentials: false + sparse-checkout: contracts + + - name: Synchronize PHP examples before postinstall generates docs + id: sdk + run: | + echo "sha=$(git -C .php-sdk-source rev-parse HEAD)" >> "$GITHUB_OUTPUT" + node scripts/sync-php-sdk-examples.mjs --source .php-sdk-source/contracts/php-sdk-examples.json + - name: Install dependencies - if: steps.bump.outputs.no_change != 'true' run: pnpm install --frozen-lockfile - name: Regenerate API docs - if: steps.bump.outputs.no_change != 'true' - run: pnpm generate:api-docs + run: | + node --test scripts/sdk-emitters.test.mjs scripts/sync-php-sdk-examples.test.mjs + pnpm generate:api-docs - name: Create Pull Request - if: steps.bump.outputs.no_change != 'true' uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "chore: bump postman submodule (${{ steps.bump.outputs.old_short }} → ${{ steps.bump.outputs.new_short }})" title: "chore: bump postman submodule" body: | - Automated bump of the `vendor/postman` submodule. + Automated bump of the `vendor/postman` submodule and matching PHP SDK examples. + + PHP SDK catalog source: `fleetbase/fleetbase-php@${{ steps.sdk.outputs.sha }}`. **Diff:** [`${{ steps.bump.outputs.old_short }}...${{ steps.bump.outputs.new_short }}`](https://github.com/fleetbase/postman/compare/${{ steps.bump.outputs.old_sha }}...${{ steps.bump.outputs.new_sha }}) @@ -99,6 +110,9 @@ jobs: branch: chore/bump-postman delete-branch: true base: main + add-paths: | + vendor/postman + scripts/php-sdk-examples.generated.json labels: | automated api-docs diff --git a/.gitignore b/.gitignore index 5c137cc4..f4fa8886 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # dependencies /node_modules +/.php-sdk-source/ /.pnp .pnp.* .yarn/* @@ -57,4 +58,3 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts - diff --git a/package.json b/package.json index 84e77655..181f67ac 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "prebuild": "node scripts/generate-api-docs.mjs", "start": "next start", "lint": "next lint", - "test": "node --experimental-strip-types --test \"src/**/*.test.ts\" && node --test scripts/sdk-emitters.test.mjs", - "test:sdk-emitters": "node --test scripts/sdk-emitters.test.mjs", + "test": "node --experimental-strip-types --test \"src/**/*.test.ts\" && node --test scripts/sdk-emitters.test.mjs scripts/sync-php-sdk-examples.test.mjs", + "test:sdk-emitters": "node --test scripts/sdk-emitters.test.mjs scripts/sync-php-sdk-examples.test.mjs", "postinstall": "fumadocs-mdx && node scripts/generate-api-docs.mjs", "content-agent:prepare": "node scripts/content-agent/prepare.mjs", "content-agent:generate-artifacts": "node scripts/content-agent/generate-artifacts.mjs", diff --git a/scripts/php-sdk-examples.generated.json b/scripts/php-sdk-examples.generated.json index 3d8ce740..b91c88ad 100644 --- a/scripts/php-sdk-examples.generated.json +++ b/scripts/php-sdk-examples.generated.json @@ -2,7 +2,7 @@ "schema_version": 1, "generated_from": { "repository": "https://github.com/fleetbase/postman", - "ref": "9b59befdc1b4623ba40bf5661bd5e88f1dac673e", + "ref": "7fe06b7c5e415a9aa292869e9037910896b049ad", "collections": [ "Fleetbase API", "Fleetbase Core API" @@ -215,8 +215,8 @@ "variables": { "deviceId": "device_id-fixture" }, - "call": "$result = $fleetbase->devices->attachDevice(\n $deviceId,\n [\n 'vehicle' => 'vehicle_id-fixture',\n ]\n);", - "code": "devices->attachDevice(\n $deviceId,\n [\n 'vehicle' => 'vehicle_id-fixture',\n ]\n);" + "call": "$result = $fleetbase->devices->attachDevice(\n $deviceId,\n [\n 'attachable_type' => 'fleet-ops:vehicle',\n 'attachable' => 'vehicle_id-fixture',\n ]\n);", + "code": "devices->attachDevice(\n $deviceId,\n [\n 'attachable_type' => 'fleet-ops:vehicle',\n 'attachable' => 'vehicle_id-fixture',\n ]\n);" }, "fleetbase-api-devices-create-a-device": { "collection": "Fleetbase API", @@ -566,6 +566,17 @@ "call": "$result = $fleetbase->entities->updateEntity(\n $entityId,\n [\n 'internal_id' => 'ENTITY001-1',\n 'description' => 'New entity description',\n 'destination' => '',\n 'sku' => 'SKUABC123',\n 'currency' => 'SGD',\n ]\n);", "code": "entities->updateEntity(\n $entityId,\n [\n 'internal_id' => 'ENTITY001-1',\n 'description' => 'New entity description',\n 'destination' => '',\n 'sku' => 'SKUABC123',\n 'currency' => 'SGD',\n ]\n);" }, + "fleetbase-api-equipment-attach-equipment": { + "collection": "Fleetbase API", + "group": "Equipment", + "name": "Attach Equipment", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::attachEquipment", + "variables": { + "equipmentId": "equipment_id-fixture" + }, + "call": "$result = $fleetbase->equipment->attachEquipment(\n $equipmentId,\n [\n 'attachable_type' => 'fleet-ops:vehicle',\n 'attachable' => 'vehicle_id-fixture',\n ]\n);", + "code": "equipment->attachEquipment(\n $equipmentId,\n [\n 'attachable_type' => 'fleet-ops:vehicle',\n 'attachable' => 'vehicle_id-fixture',\n ]\n);" + }, "fleetbase-api-equipment-create-equipment": { "collection": "Fleetbase API", "group": "Equipment", @@ -586,6 +597,17 @@ "call": "$result = $fleetbase->equipment->deleteEquipment($equipmentId);", "code": "equipment->deleteEquipment($equipmentId);" }, + "fleetbase-api-equipment-detach-equipment": { + "collection": "Fleetbase API", + "group": "Equipment", + "name": "Detach Equipment", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::detachEquipment", + "variables": { + "equipmentId": "equipment_id-fixture" + }, + "call": "$result = $fleetbase->equipment->detachEquipment($equipmentId);", + "code": "equipment->detachEquipment($equipmentId);" + }, "fleetbase-api-equipment-query-equipment": { "collection": "Fleetbase API", "group": "Equipment", @@ -1976,6 +1998,229 @@ "call": "$result = $fleetbase->trackingStatuses->updateTrackingStatus(\n $trackingStatusId,\n [\n 'country' => 'SG',\n ]\n);", "code": "trackingStatuses->updateTrackingStatus(\n $trackingStatusId,\n [\n 'country' => 'SG',\n ]\n);" }, + "fleetbase-api-trailers-attach-device-to-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Attach Device to Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\DeviceService::attachDevice", + "variables": { + "deviceId": "trailer_device_id-fixture" + }, + "call": "$result = $fleetbase->devices->attachDevice(\n $deviceId,\n [\n 'attachable_type' => 'fleet-ops:trailer',\n 'attachable' => 'trailer_id-fixture',\n ]\n);", + "code": "devices->attachDevice(\n $deviceId,\n [\n 'attachable_type' => 'fleet-ops:trailer',\n 'attachable' => 'trailer_id-fixture',\n ]\n);" + }, + "fleetbase-api-trailers-attach-equipment-to-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Attach Equipment to Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::attachEquipment", + "variables": { + "equipmentId": "trailer_equipment_id-fixture" + }, + "call": "$result = $fleetbase->equipment->attachEquipment(\n $equipmentId,\n [\n 'attachable_type' => 'fleet-ops:trailer',\n 'attachable' => 'trailer_id-fixture',\n ]\n);", + "code": "equipment->attachEquipment(\n $equipmentId,\n [\n 'attachable_type' => 'fleet-ops:trailer',\n 'attachable' => 'trailer_id-fixture',\n ]\n);" + }, + "fleetbase-api-trailers-attach-trailer-to-vehicle": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Attach Trailer to Vehicle", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::attachTrailerToVehicle", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->attachTrailerToVehicle(\n $trailerId,\n [\n 'vehicle' => 'vehicle_id-fixture',\n 'source' => 'manual',\n 'position' => 1,\n ]\n);", + "code": "trailers->attachTrailerToVehicle(\n $trailerId,\n [\n 'vehicle' => 'vehicle_id-fixture',\n 'source' => 'manual',\n 'position' => 1,\n ]\n);" + }, + "fleetbase-api-trailers-create-trailer-device": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Create Trailer Device", + "implementation": "Fleetbase\\Sdk\\Services\\DeviceService::createDevice", + "variables": [], + "call": "$result = $fleetbase->devices->createDevice(\n [\n 'name' => 'Postman Trailer Tracker',\n 'device_id' => 'POSTMAN-TRL-TRACKER',\n 'type' => 'gps',\n 'provider' => 'manual',\n ]\n);", + "code": "devices->createDevice(\n [\n 'name' => 'Postman Trailer Tracker',\n 'device_id' => 'POSTMAN-TRL-TRACKER',\n 'type' => 'gps',\n 'provider' => 'manual',\n ]\n);" + }, + "fleetbase-api-trailers-create-trailer-equipment": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Create Trailer Equipment", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::createEquipment", + "variables": [], + "call": "$result = $fleetbase->equipment->createEquipment(\n [\n 'name' => 'Postman Trailer Sensor Bracket',\n 'code' => 'POSTMAN-TRL-EQ',\n 'type' => 'tool',\n 'status' => 'available',\n ]\n);", + "code": "equipment->createEquipment(\n [\n 'name' => 'Postman Trailer Sensor Bracket',\n 'code' => 'POSTMAN-TRL-EQ',\n 'type' => 'tool',\n 'status' => 'available',\n ]\n);" + }, + "fleetbase-api-trailers-create-a-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Create a Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::createTrailer", + "variables": [], + "call": "$result = $fleetbase->trailers->createTrailer(\n [\n 'name' => 'Postman Reefer',\n 'code' => 'POSTMAN-TRL-001',\n 'type' => 'reefer',\n 'status' => 'available',\n 'plate_number' => 'PM-TR-001',\n 'payload_capacity' => 20000,\n 'axle_count' => 2,\n 'refrigerated' => true,\n 'measurement_system' => 'metric',\n ]\n);", + "code": "trailers->createTrailer(\n [\n 'name' => 'Postman Reefer',\n 'code' => 'POSTMAN-TRL-001',\n 'type' => 'reefer',\n 'status' => 'available',\n 'plate_number' => 'PM-TR-001',\n 'payload_capacity' => 20000,\n 'axle_count' => 2,\n 'refrigerated' => true,\n 'measurement_system' => 'metric',\n ]\n);" + }, + "fleetbase-api-trailers-delete-trailer-device": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Delete Trailer Device", + "implementation": "Fleetbase\\Sdk\\Services\\DeviceService::deleteDevice", + "variables": { + "deviceId": "trailer_device_id-fixture" + }, + "call": "$result = $fleetbase->devices->deleteDevice($deviceId);", + "code": "devices->deleteDevice($deviceId);" + }, + "fleetbase-api-trailers-delete-trailer-equipment": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Delete Trailer Equipment", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::deleteEquipment", + "variables": { + "equipmentId": "trailer_equipment_id-fixture" + }, + "call": "$result = $fleetbase->equipment->deleteEquipment($equipmentId);", + "code": "equipment->deleteEquipment($equipmentId);" + }, + "fleetbase-api-trailers-delete-a-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Delete a Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::deleteTrailer", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->deleteTrailer($trailerId);", + "code": "trailers->deleteTrailer($trailerId);" + }, + "fleetbase-api-trailers-detach-trailer-attachments": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Detach Trailer Attachments", + "implementation": "Fleetbase\\Sdk\\Services\\DeviceService::detachDevice", + "variables": { + "deviceId": "trailer_device_id-fixture" + }, + "call": "$result = $fleetbase->devices->detachDevice($deviceId);", + "code": "devices->detachDevice($deviceId);" + }, + "fleetbase-api-trailers-detach-trailer-equipment": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Detach Trailer Equipment", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::detachEquipment", + "variables": { + "equipmentId": "trailer_equipment_id-fixture" + }, + "call": "$result = $fleetbase->equipment->detachEquipment($equipmentId);", + "code": "equipment->detachEquipment($equipmentId);" + }, + "fleetbase-api-trailers-detach-trailer-from-vehicle": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Detach Trailer from Vehicle", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::detachTrailerFromVehicle", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->detachTrailerFromVehicle(\n $trailerId,\n [\n 'notes' => 'Postman lifecycle detach',\n ]\n);", + "code": "trailers->detachTrailerFromVehicle(\n $trailerId,\n [\n 'notes' => 'Postman lifecycle detach',\n ]\n);" + }, + "fleetbase-api-trailers-list-trailer-connections": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "List Trailer Connections", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::listTrailerConnections", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->listTrailerConnections($trailerId);", + "code": "trailers->listTrailerConnections($trailerId);" + }, + "fleetbase-api-trailers-list-vehicle-trailers": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "List Vehicle Trailers", + "implementation": "Fleetbase\\Sdk\\Services\\VehicleService::listVehicleTrailers", + "variables": { + "vehicleId": "vehicle_id-fixture" + }, + "call": "$result = $fleetbase->vehicles->listVehicleTrailers($vehicleId);", + "code": "vehicles->listVehicleTrailers($vehicleId);" + }, + "fleetbase-api-trailers-query-trailers": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Query Trailers", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::queryTrailers", + "variables": [], + "call": "$result = $fleetbase->trailers->queryTrailers(\n [\n 'query' => 'Postman Reefer',\n 'limit' => '25',\n 'sort' => '-created_at',\n ]\n);", + "code": "trailers->queryTrailers(\n [\n 'query' => 'Postman Reefer',\n 'limit' => '25',\n 'sort' => '-created_at',\n ]\n);" + }, + "fleetbase-api-trailers-reattach-equipment-to-vehicle": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Reattach Equipment to Vehicle", + "implementation": "Fleetbase\\Sdk\\Services\\EquipmentService::attachEquipment", + "variables": { + "equipmentId": "trailer_equipment_id-fixture" + }, + "call": "$result = $fleetbase->equipment->attachEquipment(\n $equipmentId,\n [\n 'attachable_type' => 'fleet-ops:vehicle',\n 'attachable' => 'vehicle_id-fixture',\n ]\n);", + "code": "equipment->attachEquipment(\n $equipmentId,\n [\n 'attachable_type' => 'fleet-ops:vehicle',\n 'attachable' => 'vehicle_id-fixture',\n ]\n);" + }, + "fleetbase-api-trailers-retrieve-a-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Retrieve a Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::retrieveTrailer", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->retrieveTrailer($trailerId);", + "code": "trailers->retrieveTrailer($trailerId);" + }, + "fleetbase-api-trailers-track-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Track Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::trackTrailer", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->trackTrailer(\n $trailerId,\n [\n 'latitude' => 40.7484,\n 'longitude' => -73.9857,\n 'speed' => 35,\n 'heading' => 90,\n ]\n);", + "code": "trailers->trackTrailer(\n $trailerId,\n [\n 'latitude' => 40.7484,\n 'longitude' => -73.9857,\n 'speed' => 35,\n 'heading' => 90,\n ]\n);" + }, + "fleetbase-api-trailers-update-a-trailer": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Update a Trailer", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::updateTrailer", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->updateTrailer(\n $trailerId,\n [\n 'name' => 'Postman Reefer Updated',\n 'status' => 'in_use',\n 'payload_capacity' => 21000,\n ]\n);", + "code": "trailers->updateTrailer(\n $trailerId,\n [\n 'name' => 'Postman Reefer Updated',\n 'status' => 'in_use',\n 'payload_capacity' => 21000,\n ]\n);" + }, + "fleetbase-api-trailers-verify-detached-connection-history": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Verify Detached Connection History", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::listTrailerConnections", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->listTrailerConnections($trailerId);", + "code": "trailers->listTrailerConnections($trailerId);" + }, + "fleetbase-api-trailers-verify-trailer-equipment": { + "collection": "Fleetbase API", + "group": "Trailers", + "name": "Verify Trailer Equipment", + "implementation": "Fleetbase\\Sdk\\Services\\TrailerService::retrieveTrailer", + "variables": { + "trailerId": "trailer_id-fixture" + }, + "call": "$result = $fleetbase->trailers->retrieveTrailer($trailerId);", + "code": "trailers->retrieveTrailer($trailerId);" + }, "fleetbase-api-vehicles-create-a-vehicle": { "collection": "Fleetbase API", "group": "Vehicles", diff --git a/scripts/sdk-emitters.test.mjs b/scripts/sdk-emitters.test.mjs index 4de989af..6aaa171e 100644 --- a/scripts/sdk-emitters.test.mjs +++ b/scripts/sdk-emitters.test.mjs @@ -75,6 +75,22 @@ test('uses positional identifiers and direct data for PHP SDK actions', () => { assert.doesNotMatch(code, /'body'\s*=>/); }); +test('uses the correct services for trailer and equipment actions', () => { + const cases = [ + ['fleetbase-api-trailers-attach-trailer-to-vehicle', 'POST', '/trailers/:id/attach', 'Attach Trailer to Vehicle', 'Trailers', /\$fleetbase->trailers->attachTrailerToVehicle\(/], + ['fleetbase-api-trailers-list-vehicle-trailers', 'GET', '/vehicles/:id/trailers', 'List Vehicle Trailers', 'Trailers', /\$fleetbase->vehicles->listVehicleTrailers\(\$vehicleId\)/], + ['fleetbase-api-trailers-attach-device-to-trailer', 'POST', '/devices/:id/attach', 'Attach Device to Trailer', 'Trailers', /\$fleetbase->devices->attachDevice\(/], + ['fleetbase-api-equipment-attach-equipment', 'POST', '/equipment/:id/attach', 'Attach Equipment', 'Equipment', /\$fleetbase->equipment->attachEquipment\(/], + ]; + for (const [id, method, url, endpointName, resourceFolder, expected] of cases) { + const code = emitPhp({ method, fullUrl: `https://api.fleetbase.io/v1${url}`, body: null, queryParams: {}, + endpointKind: 'custom-action', endpointName, rawUrl: `{{base_url}}/{{namespace}}${url}`, + resourceFolder, sdkConfig, sdkExample: catalog.examples[id] }); + assert.match(code, expected); + assert.doesNotMatch(code, /GuzzleHttp|'body'\s*=>/); + } +}); + test('uses first-class PHP SDK methods for Core API custom endpoints', () => { const code = emitPhp({ method: 'GET', diff --git a/scripts/sync-php-sdk-examples.mjs b/scripts/sync-php-sdk-examples.mjs new file mode 100644 index 00000000..7bf0baa9 --- /dev/null +++ b/scripts/sync-php-sdk-examples.mjs @@ -0,0 +1,80 @@ +import { readdir, readFile, writeFile, rename } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseArgs } from 'node:util'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const scopes = ['Fleetbase API', 'Fleetbase Core API']; +const slug = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''); + +export async function requestIds(collectionsDirectory) { + const ids = []; + async function visit(directory, segments) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + if (entry.name.startsWith('.')) continue; + const next = [...segments, entry.name]; + if (entry.isDirectory()) { + await visit(path.join(directory, entry.name), next); + } else if (entry.isFile() && entry.name.endsWith('.request.yaml')) { + ids.push(slug(next.join('-').replace(/\.request\.yaml$/, ''))); + } + } + } + for (const scope of scopes) await visit(path.join(collectionsDirectory, scope), [scope]); + if (new Set(ids).size !== ids.length) throw new Error('Postman contains duplicate PHP catalog request IDs.'); + return ids.sort(); +} + +export function validateCatalog(catalog, expectedIds) { + if (catalog?.schema_version !== 1 || catalog?.package !== 'fleetbase/fleetbase-php' || + !catalog.examples || typeof catalog.examples !== 'object' || Array.isArray(catalog.examples)) { + throw new Error('Invalid Fleetbase PHP SDK example catalog.'); + } + const expected = new Set(expectedIds); + const actual = Object.keys(catalog.examples); + const missing = expectedIds.filter((id) => !Object.hasOwn(catalog.examples, id)); + const stale = actual.filter((id) => !expected.has(id)); + const invalid = actual.filter((id) => { + const entry = catalog.examples[id]; + return !entry || !['implementation', 'call', 'code'].every((key) => typeof entry[key] === 'string' && entry[key].trim()); + }); + if (missing.length || stale.length || invalid.length) { + throw new Error( + `PHP SDK catalog does not match Postman. Missing: ${missing.join(', ') || 'none'}. ` + + `Stale: ${stale.join(', ') || 'none'}. Invalid: ${invalid.join(', ') || 'none'}. ` + + 'Merge the matching fleetbase/fleetbase-php contract update, then rerun Bump Postman Submodule.', + ); + } + return actual.length; +} + +export async function syncCatalog({ source, destination, collectionsDirectory }) { + const contents = await readFile(source, 'utf8'); + const count = validateCatalog(JSON.parse(contents), await requestIds(collectionsDirectory)); + // Validate the complete candidate before replacing the checked-in catalog. + if (path.resolve(source) !== path.resolve(destination)) { + const temporary = `${destination}.${process.pid}.tmp`; + await writeFile(temporary, contents); + await rename(temporary, destination); + } + return count; +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const { values } = parseArgs({ options: { + source: { type: 'string' }, + collections: { type: 'string' }, + } }); + const destination = path.join(root, 'scripts/php-sdk-examples.generated.json'); + try { + const count = await syncCatalog({ + source: values.source ?? destination, + destination, + collectionsDirectory: values.collections ?? path.join(root, 'vendor/postman/postman/collections'), + }); + console.log(`PHP SDK catalog verified: ${count}/${count} Postman request IDs.`); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/scripts/sync-php-sdk-examples.test.mjs b/scripts/sync-php-sdk-examples.test.mjs new file mode 100644 index 00000000..54b114b5 --- /dev/null +++ b/scripts/sync-php-sdk-examples.test.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { requestIds, syncCatalog, validateCatalog } from './sync-php-sdk-examples.mjs'; + +const example = { implementation: 'Fleetbase\\Sdk\\Services\\TrailerService::createTrailer', call: '$fleetbase->trailers->createTrailer([]);', code: ' ({ schema_version: 1, package: 'fleetbase/fleetbase-php', examples }); + +test('requires complete, non-stale, executable SDK mappings', () => { + assert.equal(validateCatalog(catalog({ trailer: example }), ['trailer']), 1); + assert.throws(() => validateCatalog(catalog({ old: example }), ['trailer']), /Missing: trailer.*Stale: old/); + assert.throws(() => validateCatalog(catalog({ trailer: {} }), ['trailer']), /Invalid: trailer/); + assert.throws(() => validateCatalog({}, []), /Invalid Fleetbase/); +}); + +test('sync uses stable IDs from both PHP collections and never overwrites with an incomplete catalog', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'php-catalog-test-')); + try { + for (const group of ['Fleetbase API/Trailers', 'Fleetbase Core API/Organizations', 'Fleetbase API/Trailers/.resources']) { + await mkdir(path.join(directory, group), { recursive: true }); + } + await writeFile(path.join(directory, 'Fleetbase API/Trailers/Create a Trailer.request.yaml'), 'method: POST'); + await writeFile(path.join(directory, 'Fleetbase API/Trailers/Create a Trailer.params.yaml'), '{}'); + await writeFile(path.join(directory, 'Fleetbase API/Trailers/.resources/Ignore.request.yaml'), '{}'); + await writeFile(path.join(directory, 'Fleetbase Core API/Organizations/Get Current Organization.request.yaml'), 'method: GET'); + const ids = await requestIds(directory); + assert.deepEqual(ids, ['fleetbase-api-trailers-create-a-trailer', 'fleetbase-core-api-organizations-get-current-organization']); + const source = path.join(directory, 'source.json'); + const destination = path.join(directory, 'destination.json'); + await writeFile(destination, 'existing catalog'); + await writeFile(source, JSON.stringify(catalog({}))); + await assert.rejects(syncCatalog({ source, destination, collectionsDirectory: directory }), /Missing:/); + assert.equal(await readFile(destination, 'utf8'), 'existing catalog'); + const contents = JSON.stringify(catalog(Object.fromEntries(ids.map((id) => [id, example])))); + await writeFile(source, contents); + assert.equal(await syncCatalog({ source, destination, collectionsDirectory: directory }), 2); + assert.equal(await readFile(destination, 'utf8'), contents); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/vendor/postman b/vendor/postman index 9b59befd..7fe06b7c 160000 --- a/vendor/postman +++ b/vendor/postman @@ -1 +1 @@ -Subproject commit 9b59befdc1b4623ba40bf5661bd5e88f1dac673e +Subproject commit 7fe06b7c5e415a9aa292869e9037910896b049ad