diff --git a/examples/ship-happens/schema/interplanetary.json b/examples/ship-happens/schema/interplanetary.json index 836fd9d4..49edb30a 100644 --- a/examples/ship-happens/schema/interplanetary.json +++ b/examples/ship-happens/schema/interplanetary.json @@ -99,11 +99,7 @@ }, "Cargo": { "type": "object", - "required": [ - "weight", - "volume", - "category" - ], + "required": ["weight", "volume", "category"], "properties": { "weight": { "type": "number", @@ -263,4 +259,4 @@ } } } -} \ No newline at end of file +} diff --git a/examples/ship-happens/schema/label-v3.json b/examples/ship-happens/schema/label-v3.json index cbf586a6..ce91e0ca 100644 --- a/examples/ship-happens/schema/label-v3.json +++ b/examples/ship-happens/schema/label-v3.json @@ -509,7 +509,21 @@ "description": "The desired format of the label", "schema": { "type": "string", - "enum": ["PDF", "PNG", "ZPL", "JPEG", "TIFF", "SVG", "EPS", "BMP", "GIF", "WEBP", "PCX", "EMF", "PS"], + "enum": [ + "PDF", + "PNG", + "ZPL", + "JPEG", + "TIFF", + "SVG", + "EPS", + "BMP", + "GIF", + "WEBP", + "PCX", + "EMF", + "PS" + ], "default": "PDF" } } diff --git a/examples/ship-happens/schema/shipments.json b/examples/ship-happens/schema/shipments.json index a4659625..693675b9 100644 --- a/examples/ship-happens/schema/shipments.json +++ b/examples/ship-happens/schema/shipments.json @@ -1,168 +1,247 @@ { - "openapi": "3.0.3", - "info": { - "title": "Shipment API", - "description": "This API allows you to create and track shipments through the Ship Happens platform.\n\n## Authentication\nAll endpoints require a valid API key passed in the `X-API-Key` header.\n", - "version": "1.0.0", - "contact": { - "name": "Ship Happens API Support", - "email": "api@sh.example.com", - "url": "https://developers.sh.example.com" + "openapi": "3.0.3", + "info": { + "title": "Shipment API", + "description": "This API allows you to create and track shipments through the Ship Happens platform.\n\n## Authentication\nAll endpoints require a valid API key passed in the `X-API-Key` header.\n", + "version": "1.0.0", + "contact": { + "name": "Ship Happens API Support", + "email": "api@sh.example.com", + "url": "https://developers.sh.example.com" + } + }, + "servers": [ + { + "url": "https://api.sh.example.com/v1", + "description": "Production environment" + }, + { + "url": "https://api.staging.sh.example.com/v1", + "description": "Staging environment" + }, + { + "url": "https://api.dev.sh.example.com/v1", + "description": "Development environment" + } + ], + "security": [ + { + "ApiKeyAuth": [] + } + ], + "components": { + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" } }, - "servers": [ - { - "url": "https://api.sh.example.com/v1", - "description": "Production environment" - }, - { - "url": "https://api.staging.sh.example.com/v1", - "description": "Staging environment" + "schemas": { + "Shipment": { + "type": "object", + "required": ["recipientAddress", "senderAddress", "packages"], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "recipientAddress": { + "$ref": "#/components/schemas/Address" + }, + "senderAddress": { + "$ref": "#/components/schemas/Address" + }, + "packages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Package" + } + }, + "status": { + "type": "string", + "enum": ["CREATED", "IN_TRANSIT", "DELIVERED", "EXCEPTION"], + "readOnly": true + }, + "trackingNumber": { + "type": "string", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + } + } }, - { - "url": "https://api.dev.sh.example.com/v1", - "description": "Development environment" - } - ], - "security": [ - { - "ApiKeyAuth": [] - } - ], - "components": { - "securitySchemes": { - "ApiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "X-API-Key" + "Address": { + "type": "object", + "required": ["street", "city", "country", "postalCode"], + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "country": { + "type": "string" + }, + "postalCode": { + "type": "string" + } } }, - "schemas": { - "Shipment": { - "type": "object", - "required": ["recipientAddress", "senderAddress", "packages"], - "properties": { - "id": { - "type": "string", - "format": "uuid", - "readOnly": true - }, - "recipientAddress": { - "$ref": "#/components/schemas/Address" - }, - "senderAddress": { - "$ref": "#/components/schemas/Address" - }, - "packages": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Package" - } - }, - "status": { - "type": "string", - "enum": ["CREATED", "IN_TRANSIT", "DELIVERED", "EXCEPTION"], - "readOnly": true - }, - "trackingNumber": { - "type": "string", - "readOnly": true - }, - "createdAt": { - "type": "string", - "format": "date-time", - "readOnly": true - } + "Package": { + "type": "object", + "required": ["weight", "dimensions"], + "properties": { + "weight": { + "type": "number", + "format": "float", + "description": "Weight in kilograms" + }, + "dimensions": { + "$ref": "#/components/schemas/Dimensions" } - }, - "Address": { - "type": "object", - "required": ["street", "city", "country", "postalCode"], - "properties": { - "street": { - "type": "string" - }, - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "country": { - "type": "string" - }, - "postalCode": { - "type": "string" - } + } + }, + "Dimensions": { + "type": "object", + "required": ["length", "width", "height"], + "properties": { + "length": { + "type": "number", + "format": "float", + "description": "Length in centimeters" + }, + "width": { + "type": "number", + "format": "float", + "description": "Width in centimeters" + }, + "height": { + "type": "number", + "format": "float", + "description": "Height in centimeters" } - }, - "Package": { - "type": "object", - "required": ["weight", "dimensions"], - "properties": { - "weight": { - "type": "number", - "format": "float", - "description": "Weight in kilograms" - }, - "dimensions": { - "$ref": "#/components/schemas/Dimensions" - } + } + }, + "Error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" } - }, - "Dimensions": { - "type": "object", - "required": ["length", "width", "height"], - "properties": { - "length": { - "type": "number", - "format": "float", - "description": "Length in centimeters" - }, - "width": { - "type": "number", - "format": "float", - "description": "Width in centimeters" - }, - "height": { - "type": "number", - "format": "float", - "description": "Height in centimeters" + } + } + } + }, + "paths": { + "/shipments": { + "post": { + "tags": ["Shipment Management"], + "summary": "Create a new shipment", + "description": "Creates a new shipment with the provided details", + "operationId": "createShipment", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipment" + }, + "examples": { + "simple": { + "summary": "Simple domestic shipment", + "value": { + "recipientAddress": { + "street": "123 Delivery St", + "city": "Shiptown", + "state": "ST", + "country": "US", + "postalCode": "12345" + }, + "senderAddress": { + "street": "456 Sender Ave", + "city": "Packageville", + "state": "ST", + "country": "US", + "postalCode": "67890" + }, + "packages": [ + { + "weight": 2.5, + "dimensions": { + "length": 30, + "width": 20, + "height": 15 + } + } + ] + } + }, + "international": { + "summary": "International multi-package shipment", + "value": { + "recipientAddress": { + "street": "789 Global Road", + "city": "London", + "country": "GB", + "postalCode": "SW1A 1AA" + }, + "senderAddress": { + "street": "321 Export Blvd", + "city": "Los Angeles", + "state": "CA", + "country": "US", + "postalCode": "90001" + }, + "packages": [ + { + "weight": 1.2, + "dimensions": { + "length": 25, + "width": 15, + "height": 10 + } + }, + { + "weight": 3.8, + "dimensions": { + "length": 40, + "width": 30, + "height": 20 + } + } + ] + } + } + } } } }, - "Error": { - "type": "object", - "required": ["code", "message"], - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - } - } - } - } - }, - "paths": { - "/shipments": { - "post": { - "tags": ["Shipment Management"], - "summary": "Create a new shipment", - "description": "Creates a new shipment with the provided details", - "operationId": "createShipment", - "requestBody": { - "required": true, + "responses": { + "201": { + "description": "Shipment created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Shipment" }, "examples": { - "simple": { - "summary": "Simple domestic shipment", + "domestic": { + "summary": "Domestic shipment response", "value": { + "id": "123e4567-e89b-12d3-a456-426614174000", "recipientAddress": { "street": "123 Delivery St", "city": "Shiptown", @@ -186,12 +265,16 @@ "height": 15 } } - ] + ], + "status": "CREATED", + "trackingNumber": "SH123456789", + "createdAt": "2025-01-09T12:00:00Z" } }, "international": { - "summary": "International multi-package shipment", + "summary": "International shipment response", "value": { + "id": "987fcdeb-a654-3210-9876-543210987654", "recipientAddress": { "street": "789 Global Road", "city": "London", @@ -222,232 +305,148 @@ "height": 20 } } - ] + ], + "status": "CREATED", + "trackingNumber": "SH987654321", + "createdAt": "2025-01-09T14:30:00Z" } } } } } }, - "responses": { - "201": { - "description": "Shipment created successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Shipment" - }, - "examples": { - "domestic": { - "summary": "Domestic shipment response", - "value": { - "id": "123e4567-e89b-12d3-a456-426614174000", - "recipientAddress": { - "street": "123 Delivery St", - "city": "Shiptown", - "state": "ST", - "country": "US", - "postalCode": "12345" - }, - "senderAddress": { - "street": "456 Sender Ave", - "city": "Packageville", - "state": "ST", - "country": "US", - "postalCode": "67890" - }, - "packages": [ - { - "weight": 2.5, - "dimensions": { - "length": 30, - "width": 20, - "height": 15 - } - } - ], - "status": "CREATED", - "trackingNumber": "SH123456789", - "createdAt": "2025-01-09T12:00:00Z" - } - }, - "international": { - "summary": "International shipment response", - "value": { - "id": "987fcdeb-a654-3210-9876-543210987654", - "recipientAddress": { - "street": "789 Global Road", - "city": "London", - "country": "GB", - "postalCode": "SW1A 1AA" - }, - "senderAddress": { - "street": "321 Export Blvd", - "city": "Los Angeles", - "state": "CA", - "country": "US", - "postalCode": "90001" - }, - "packages": [ - { - "weight": 1.2, - "dimensions": { - "length": 25, - "width": 15, - "height": 10 - } - }, - { - "weight": 3.8, - "dimensions": { - "length": 40, - "width": 30, - "height": 20 - } - } - ], - "status": "CREATED", - "trackingNumber": "SH987654321", - "createdAt": "2025-01-09T14:30:00Z" - } - } - } - } - } - }, - "400": { - "description": "Invalid input", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "code": "INVALID_INPUT", - "message": "Invalid recipient address provided" - } + "400": { + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "code": "INVALID_INPUT", + "message": "Invalid recipient address provided" } } } } } - }, - "/shipments/{trackingNumber}": { - "get": { - "tags": ["Shipment Management"], - "summary": "Track a shipment", - "description": "Get the current status and tracking information for a shipment", - "operationId": "trackShipment", - "parameters": [ - { - "name": "trackingNumber", - "in": "path", - "required": true, - "schema": { - "type": "string" - }, - "example": "SH123456789" - } - ], - "responses": { - "200": { - "description": "Shipment tracking information retrieved successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Shipment" + } + }, + "/shipments/{trackingNumber}": { + "get": { + "tags": ["Shipment Management"], + "summary": "Track a shipment", + "description": "Get the current status and tracking information for a shipment", + "operationId": "trackShipment", + "parameters": [ + { + "name": "trackingNumber", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "example": "SH123456789" + } + ], + "responses": { + "200": { + "description": "Shipment tracking information retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Shipment" + }, + "example": { + "id": "123e4567-e89b-12d3-a456-426614174000", + "recipientAddress": { + "street": "123 Delivery St", + "city": "Shiptown", + "state": "ST", + "country": "US", + "postalCode": "12345" }, - "example": { - "id": "123e4567-e89b-12d3-a456-426614174000", - "recipientAddress": { - "street": "123 Delivery St", - "city": "Shiptown", - "state": "ST", - "country": "US", - "postalCode": "12345" - }, - "senderAddress": { - "street": "456 Sender Ave", - "city": "Packageville", - "state": "ST", - "country": "US", - "postalCode": "67890" - }, - "packages": [ - { - "weight": 2.5, - "dimensions": { - "length": 30, - "width": 20, - "height": 15 - } + "senderAddress": { + "street": "456 Sender Ave", + "city": "Packageville", + "state": "ST", + "country": "US", + "postalCode": "67890" + }, + "packages": [ + { + "weight": 2.5, + "dimensions": { + "length": 30, + "width": 20, + "height": 15 } - ], - "status": "IN_TRANSIT", - "trackingNumber": "SH123456789", - "createdAt": "2025-01-09T12:00:00Z" - } + } + ], + "status": "IN_TRANSIT", + "trackingNumber": "SH123456789", + "createdAt": "2025-01-09T12:00:00Z" } } - }, - "404": { - "description": "Shipment not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "code": "NOT_FOUND", - "message": "Shipment with tracking number SH123456789 not found" - } + } + }, + "404": { + "description": "Shipment not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "code": "NOT_FOUND", + "message": "Shipment with tracking number SH123456789 not found" } } - }, - "delete": { - "tags": ["Shipment Management"], - "summary": "Cancel shipment", - "description": "Cancel a shipment that hasn't been picked up yet", - "operationId": "cancelShipment", - "parameters": [ - { - "name": "trackingNumber", - "in": "path", - "required": true, - "schema": { - "type": "string" - } + } + }, + "delete": { + "tags": ["Shipment Management"], + "summary": "Cancel shipment", + "description": "Cancel a shipment that hasn't been picked up yet", + "operationId": "cancelShipment", + "parameters": [ + { + "name": "trackingNumber", + "in": "path", + "required": true, + "schema": { + "type": "string" } - ], - "responses": { - "200": { - "description": "Shipment cancelled successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["CANCELLED"] - }, - "refundAmount": { - "type": "number", - "format": "float" - }, - "currency": { - "type": "string" - } + } + ], + "responses": { + "200": { + "description": "Shipment cancelled successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["CANCELLED"] + }, + "refundAmount": { + "type": "number", + "format": "float" + }, + "currency": { + "type": "string" } - }, - "examples": { - "full_refund": { - "summary": "Full refund issued", - "value": { - "status": "CANCELLED", - "refundAmount": 116.74, - "currency": "USD" - } + } + }, + "examples": { + "full_refund": { + "summary": "Full refund issued", + "value": { + "status": "CANCELLED", + "refundAmount": 116.74, + "currency": "USD" } } } @@ -457,1120 +456,1120 @@ } } } - }, - "/shipments/{shipmentId}/hold": { - "put": { - "tags": ["Shipment Management"], - "summary": "Hold shipment", - "description": "Place a shipment on hold at a facility", - "operationId": "holdShipment", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, + } + }, + "/shipments/{shipmentId}/hold": { + "put": { + "tags": ["Shipment Management"], + "summary": "Hold shipment", + "description": "Place a shipment on hold at a facility", + "operationId": "holdShipment", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { "schema": { - "type": "string", - "format": "uuid" + "type": "object", + "required": ["holdUntil"], + "properties": { + "holdUntil": { + "type": "string", + "format": "date-time" + }, + "reason": { + "type": "string", + "enum": [ + "RECIPIENT_REQUEST", + "CUSTOMS_HOLD", + "WEATHER_DELAY" + ] + }, + "facilityId": { + "type": "string" + } + } + }, + "examples": { + "recipient_request": { + "summary": "Hold at facility per recipient request", + "value": { + "holdUntil": "2025-01-15T17:00:00Z", + "reason": "RECIPIENT_REQUEST", + "facilityId": "LAX1" + } + } } } - ], - "requestBody": { - "required": true, + } + }, + "responses": { + "200": { + "description": "Shipment placed on hold successfully", "content": { "application/json": { "schema": { "type": "object", - "required": ["holdUntil"], "properties": { - "holdUntil": { + "status": { "type": "string", - "format": "date-time" - }, - "reason": { - "type": "string", - "enum": [ - "RECIPIENT_REQUEST", - "CUSTOMS_HOLD", - "WEATHER_DELAY" - ] + "enum": ["ON_HOLD"] }, - "facilityId": { + "holdLocation": { "type": "string" + }, + "holdUntil": { + "type": "string", + "format": "date-time" } } }, - "examples": { - "recipient_request": { - "summary": "Hold at facility per recipient request", - "value": { - "holdUntil": "2025-01-15T17:00:00Z", - "reason": "RECIPIENT_REQUEST", - "facilityId": "LAX1" - } - } + "example": { + "status": "ON_HOLD", + "holdLocation": "LAX1 - Los Angeles Hub", + "holdUntil": "2025-01-15T17:00:00Z" } } } - }, - "responses": { - "200": { - "description": "Shipment placed on hold successfully", - "content": { - "application/json": { - "schema": { + } + } + } + }, + "/shipments/{shipmentId}/rates": { + "post": { + "tags": ["Rates & Billing"], + "summary": "Calculate shipping rates", + "description": "Calculate available shipping rates for a shipment based on service level, destination, and package details", + "operationId": "calculateRates", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["serviceLevel"], + "properties": { + "serviceLevel": { + "type": "string", + "enum": ["ECONOMY", "STANDARD", "EXPRESS", "SAME_DAY"] + }, + "insurance": { "type": "object", "properties": { - "status": { - "type": "string", - "enum": ["ON_HOLD"] - }, - "holdLocation": { - "type": "string" + "value": { + "type": "number", + "format": "float", + "description": "Declared value for insurance in USD" }, - "holdUntil": { + "description": { "type": "string", - "format": "date-time" + "description": "Description of insured items" } } - }, - "example": { - "status": "ON_HOLD", - "holdLocation": "LAX1 - Los Angeles Hub", - "holdUntil": "2025-01-15T17:00:00Z" + } + } + }, + "examples": { + "basic": { + "summary": "Basic service level", + "value": { + "serviceLevel": "STANDARD" + } + }, + "insured": { + "summary": "Express with insurance", + "description": "Express shipping with insurance for high-value electronics", + "value": { + "serviceLevel": "EXPRESS", + "insurance": { + "value": 1500.0, + "description": "Gaming laptop with accessories" + } } } } } } - } - }, - "/shipments/{shipmentId}/rates": { - "post": { - "tags": ["Rates & Billing"], - "summary": "Calculate shipping rates", - "description": "Calculate available shipping rates for a shipment based on service level, destination, and package details", - "operationId": "calculateRates", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "Rates calculated successfully", "content": { "application/json": { "schema": { "type": "object", - "required": ["serviceLevel"], "properties": { - "serviceLevel": { - "type": "string", - "enum": ["ECONOMY", "STANDARD", "EXPRESS", "SAME_DAY"] + "baseRate": { + "type": "number", + "format": "float" }, - "insurance": { + "fees": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "amount": { + "type": "number", + "format": "float" + }, + "description": { + "type": "string" + } + } + } + }, + "totalRate": { + "type": "number", + "format": "float" + }, + "currency": { + "type": "string" + }, + "transitTime": { "type": "object", "properties": { - "value": { - "type": "number", - "format": "float", - "description": "Declared value for insurance in USD" + "min": { + "type": "integer" }, - "description": { + "max": { + "type": "integer" + }, + "unit": { "type": "string", - "description": "Description of insured items" + "enum": ["HOURS", "DAYS"] } } } } }, "examples": { - "basic": { - "summary": "Basic service level", + "domestic_ground": { + "summary": "Domestic ground shipping", "value": { - "serviceLevel": "STANDARD" + "baseRate": 15.99, + "fees": [ + { + "type": "FUEL_SURCHARGE", + "amount": 1.2, + "description": "Current fuel surcharge" + } + ], + "totalRate": 17.19, + "currency": "USD", + "transitTime": { + "min": 3, + "max": 5, + "unit": "DAYS" + } } }, - "insured": { - "summary": "Express with insurance", - "description": "Express shipping with insurance for high-value electronics", + "international_express": { + "summary": "International express with insurance", "value": { - "serviceLevel": "EXPRESS", - "insurance": { - "value": 1500.0, - "description": "Gaming laptop with accessories" + "baseRate": 89.99, + "fees": [ + { + "type": "FUEL_SURCHARGE", + "amount": 6.75, + "description": "Current fuel surcharge" + }, + { + "type": "INSURANCE", + "amount": 15.0, + "description": "Insurance for declared value of $1,500.00" + }, + { + "type": "REMOTE_AREA", + "amount": 5.0, + "description": "Remote area delivery fee" + } + ], + "totalRate": 116.74, + "currency": "USD", + "transitTime": { + "min": 24, + "max": 48, + "unit": "HOURS" } } } } } } - }, - "responses": { - "200": { - "description": "Rates calculated successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "baseRate": { - "type": "number", - "format": "float" - }, - "fees": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "amount": { - "type": "number", - "format": "float" - }, - "description": { - "type": "string" - } - } - } - }, - "totalRate": { - "type": "number", - "format": "float" - }, - "currency": { - "type": "string" - }, - "transitTime": { - "type": "object", - "properties": { - "min": { - "type": "integer" - }, - "max": { - "type": "integer" - }, - "unit": { - "type": "string", - "enum": ["HOURS", "DAYS"] - } - } - } - } + } + } + } + }, + "/shipments/{shipmentId}/insurance": { + "post": { + "tags": ["Rates & Billing"], + "summary": "Add insurance", + "description": "Add or modify insurance coverage for a shipment", + "operationId": "addInsurance", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["coverage"], + "properties": { + "coverage": { + "type": "number", + "format": "float" }, - "examples": { - "domestic_ground": { - "summary": "Domestic ground shipping", - "value": { - "baseRate": 15.99, - "fees": [ - { - "type": "FUEL_SURCHARGE", - "amount": 1.2, - "description": "Current fuel surcharge" - } - ], - "totalRate": 17.19, - "currency": "USD", - "transitTime": { - "min": 3, - "max": 5, - "unit": "DAYS" - } - } - }, - "international_express": { - "summary": "International express with insurance", - "value": { - "baseRate": 89.99, - "fees": [ - { - "type": "FUEL_SURCHARGE", - "amount": 6.75, - "description": "Current fuel surcharge" - }, - { - "type": "INSURANCE", - "amount": 15.0, - "description": "Insurance for declared value of $1,500.00" - }, - { - "type": "REMOTE_AREA", - "amount": 5.0, - "description": "Remote area delivery fee" - } - ], - "totalRate": 116.74, - "currency": "USD", - "transitTime": { - "min": 24, - "max": 48, - "unit": "HOURS" + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "value": { + "type": "number", + "format": "float" } } } } } + }, + "examples": { + "electronics": { + "summary": "Insurance for electronics", + "value": { + "coverage": 2500.0, + "items": [ + { + "description": "MacBook Pro 16\"", + "value": 2000.0 + }, + { + "description": "Apple Magic Keyboard", + "value": 300.0 + }, + { + "description": "Apple Magic Mouse", + "value": 200.0 + } + ] + } + } } } } - } - }, - "/shipments/{shipmentId}/insurance": { - "post": { - "tags": ["Rates & Billing"], - "summary": "Add insurance", - "description": "Add or modify insurance coverage for a shipment", - "operationId": "addInsurance", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "Insurance added successfully", "content": { "application/json": { "schema": { "type": "object", - "required": ["coverage"], "properties": { + "premium": { + "type": "number", + "format": "float" + }, "coverage": { "type": "number", "format": "float" }, - "items": { - "type": "array", - "items": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "value": { - "type": "number", - "format": "float" - } - } - } + "policyNumber": { + "type": "string" } } }, - "examples": { - "electronics": { - "summary": "Insurance for electronics", - "value": { - "coverage": 2500.0, - "items": [ - { - "description": "MacBook Pro 16\"", - "value": 2000.0 + "example": { + "premium": 75.0, + "coverage": 2500.0, + "policyNumber": "INS-123456" + } + } + } + } + } + } + }, + "/shipments/{shipmentId}/customs": { + "put": { + "tags": ["International Shipping"], + "summary": "Update customs documentation", + "description": "Update or add customs documentation for international shipments", + "operationId": "updateCustoms", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["items"], + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "required": [ + "description", + "quantity", + "value", + "hsCode", + "originCountry" + ], + "properties": { + "description": { + "type": "string" }, - { - "description": "Apple Magic Keyboard", - "value": 300.0 + "quantity": { + "type": "integer", + "minimum": 1 }, - { - "description": "Apple Magic Mouse", - "value": 200.0 + "value": { + "type": "number", + "format": "float" + }, + "weight": { + "type": "number", + "format": "float" + }, + "hsCode": { + "type": "string", + "pattern": "^[0-9]{6,10}$" + }, + "originCountry": { + "type": "string", + "pattern": "^[A-Z]{2}$" } - ] + } } + }, + "purpose": { + "type": "string", + "enum": [ + "COMMERCIAL", + "PERSONAL", + "GIFT", + "RETURN", + "REPAIR" + ] + }, + "incoterm": { + "type": "string", + "enum": ["DAP", "DDP", "FCA", "EXW"] } } - } - } - }, - "responses": { - "200": { - "description": "Insurance added successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "premium": { - "type": "number", - "format": "float" + }, + "examples": { + "commercial": { + "summary": "Commercial electronics shipment", + "value": { + "items": [ + { + "description": "Smartphone", + "quantity": 10, + "value": 399.99, + "weight": 0.18, + "hsCode": "851712", + "originCountry": "CN" }, - "coverage": { - "type": "number", - "format": "float" + { + "description": "Protective Cases", + "quantity": 10, + "value": 9.99, + "weight": 0.05, + "hsCode": "392690", + "originCountry": "CN" + } + ], + "purpose": "COMMERCIAL", + "incoterm": "DDP" + } + }, + "gift": { + "summary": "Personal gift shipment", + "description": "Handmade items sent as a gift", + "value": { + "items": [ + { + "description": "Handmade Wool Sweater", + "quantity": 1, + "value": 75.0, + "weight": 0.5, + "hsCode": "611010", + "originCountry": "IE" }, - "policyNumber": { - "type": "string" + { + "description": "Local Chocolate Assortment", + "quantity": 2, + "value": 25.0, + "weight": 0.3, + "hsCode": "180632", + "originCountry": "IE" } - } - }, - "example": { - "premium": 75.0, - "coverage": 2500.0, - "policyNumber": "INS-123456" + ], + "purpose": "GIFT", + "incoterm": "DAP" } } } } } - } - }, - "/shipments/{shipmentId}/customs": { - "put": { - "tags": ["International Shipping"], - "summary": "Update customs documentation", - "description": "Update or add customs documentation for international shipments", - "operationId": "updateCustoms", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { - "required": true, + }, + "responses": { + "200": { + "description": "Customs documentation updated successfully", "content": { "application/json": { "schema": { "type": "object", - "required": ["items"], "properties": { - "items": { + "id": { + "type": "string", + "format": "uuid" + }, + "status": { + "type": "string", + "enum": ["PENDING", "APPROVED", "REJECTED"] + }, + "customsValue": { + "type": "number", + "format": "float" + }, + "currency": { + "type": "string" + }, + "documents": { "type": "array", "items": { "type": "object", - "required": [ - "description", - "quantity", - "value", - "hsCode", - "originCountry" - ], "properties": { - "description": { - "type": "string" - }, - "quantity": { - "type": "integer", - "minimum": 1 - }, - "value": { - "type": "number", - "format": "float" - }, - "weight": { - "type": "number", - "format": "float" - }, - "hsCode": { + "type": { "type": "string", - "pattern": "^[0-9]{6,10}$" + "enum": [ + "COMMERCIAL_INVOICE", + "DECLARATION", + "CERTIFICATE_ORIGIN" + ] }, - "originCountry": { + "url": { "type": "string", - "pattern": "^[A-Z]{2}$" + "format": "uri" } } } - }, - "purpose": { - "type": "string", - "enum": [ - "COMMERCIAL", - "PERSONAL", - "GIFT", - "RETURN", - "REPAIR" - ] - }, - "incoterm": { - "type": "string", - "enum": ["DAP", "DDP", "FCA", "EXW"] } } }, "examples": { - "commercial": { - "summary": "Commercial electronics shipment", + "commercial_approved": { + "summary": "Approved commercial shipment", "value": { - "items": [ + "id": "a1b2c3d4-e5f6-4a5b-9c8d-1a2b3c4d5e6f", + "status": "APPROVED", + "customsValue": 4099.8, + "currency": "USD", + "documents": [ { - "description": "Smartphone", - "quantity": 10, - "value": 399.99, - "weight": 0.18, - "hsCode": "851712", - "originCountry": "CN" + "type": "COMMERCIAL_INVOICE", + "url": "https://api.sh.example.com/v1/customs/docs/invoice_12345.pdf" }, { - "description": "Protective Cases", - "quantity": 10, - "value": 9.99, - "weight": 0.05, - "hsCode": "392690", - "originCountry": "CN" + "type": "DECLARATION", + "url": "https://api.sh.example.com/v1/customs/docs/declaration_12345.pdf" } - ], - "purpose": "COMMERCIAL", - "incoterm": "DDP" + ] } }, - "gift": { - "summary": "Personal gift shipment", - "description": "Handmade items sent as a gift", + "gift_pending": { + "summary": "Pending gift shipment", "value": { - "items": [ - { - "description": "Handmade Wool Sweater", - "quantity": 1, - "value": 75.0, - "weight": 0.5, - "hsCode": "611010", - "originCountry": "IE" - }, + "id": "f6e5d4c3-b2a1-4c5d-8e9f-2b3a4c5d6e7f", + "status": "PENDING", + "customsValue": 125.0, + "currency": "USD", + "documents": [ { - "description": "Local Chocolate Assortment", - "quantity": 2, - "value": 25.0, - "weight": 0.3, - "hsCode": "180632", - "originCountry": "IE" + "type": "DECLARATION", + "url": "https://api.sh.example.com/v1/customs/docs/declaration_67890.pdf" } - ], - "purpose": "GIFT", - "incoterm": "DAP" + ] } } } } } - }, - "responses": { - "200": { - "description": "Customs documentation updated successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "status": { - "type": "string", - "enum": ["PENDING", "APPROVED", "REJECTED"] - }, - "customsValue": { - "type": "number", - "format": "float" - }, - "currency": { - "type": "string" - }, - "documents": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "COMMERCIAL_INVOICE", - "DECLARATION", - "CERTIFICATE_ORIGIN" - ] - }, - "url": { - "type": "string", - "format": "uri" - } - } - } - } - } + } + } + } + }, + "/shipments/{shipmentId}/customs/duties": { + "post": { + "tags": ["International Shipping"], + "summary": "Pay import duties", + "description": "Pay import duties and taxes for an international shipment", + "operationId": "payDuties", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["paymentMethod"], + "properties": { + "paymentMethod": { + "type": "string", + "enum": ["CREDIT_CARD", "BANK_TRANSFER", "ACCOUNT_BALANCE"] }, - "examples": { - "commercial_approved": { - "summary": "Approved commercial shipment", - "value": { - "id": "a1b2c3d4-e5f6-4a5b-9c8d-1a2b3c4d5e6f", - "status": "APPROVED", - "customsValue": 4099.8, - "currency": "USD", - "documents": [ - { - "type": "COMMERCIAL_INVOICE", - "url": "https://api.sh.example.com/v1/customs/docs/invoice_12345.pdf" - }, - { - "type": "DECLARATION", - "url": "https://api.sh.example.com/v1/customs/docs/declaration_12345.pdf" - } - ] - } - }, - "gift_pending": { - "summary": "Pending gift shipment", - "value": { - "id": "f6e5d4c3-b2a1-4c5d-8e9f-2b3a4c5d6e7f", - "status": "PENDING", - "customsValue": 125.0, - "currency": "USD", - "documents": [ - { - "type": "DECLARATION", - "url": "https://api.sh.example.com/v1/customs/docs/declaration_67890.pdf" - } - ] - } + "paymentDetails": { + "type": "object", + "additionalProperties": true + } + } + }, + "examples": { + "credit_card": { + "summary": "Pay with credit card", + "value": { + "paymentMethod": "CREDIT_CARD", + "paymentDetails": { + "last4": "4242", + "brand": "visa" } } } } } } - } - }, - "/shipments/{shipmentId}/customs/duties": { - "post": { - "tags": ["International Shipping"], - "summary": "Pay import duties", - "description": "Pay import duties and taxes for an international shipment", - "operationId": "payDuties", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" + }, + "responses": { + "200": { + "description": "Duties paid successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "amount": { + "type": "number", + "format": "float" + }, + "currency": { + "type": "string" + }, + "receipt": { + "type": "string", + "format": "uri" + } + } + }, + "example": { + "amount": 125.5, + "currency": "GBP", + "receipt": "https://api.sh.example.com/v1/receipts/duty_123456.pdf" + } } } - ], - "requestBody": { + } + } + } + }, + "/shipments/{shipmentId}/label": { + "get": { + "tags": ["Documentation"], + "summary": "Get shipping label", + "description": "Get the shipping label for a shipment in various formats. Supports both JSON and XML responses. XML format follows the EDIFACT D96A standard for shipping label interchange, while JSON is provided for modern API integrations.", + "operationId": "getLabel", + "parameters": [ + { + "name": "shipmentId", + "in": "path", "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": ["PDF", "PNG", "ZPL"] + }, + "description": "Label format" + }, + { + "name": "Accept", + "in": "header", + "schema": { + "type": "string", + "enum": ["application/json", "application/xml"], + "default": "application/json" + }, + "description": "Response format. Use application/xml for EDI-compliant responses following EDIFACT D96A standard." + } + ], + "responses": { + "200": { + "description": "Label generated successfully", "content": { "application/json": { "schema": { "type": "object", - "required": ["paymentMethod"], + "required": ["shipmentId", "format"], "properties": { - "paymentMethod": { + "id": { "type": "string", - "enum": ["CREDIT_CARD", "BANK_TRANSFER", "ACCOUNT_BALANCE"] + "format": "uuid", + "readOnly": true }, - "paymentDetails": { - "type": "object", - "additionalProperties": true + "shipmentId": { + "type": "string", + "format": "uuid" + }, + "format": { + "type": "string", + "enum": ["PDF", "PNG", "ZPL"] + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "readOnly": true } } }, "examples": { - "credit_card": { - "summary": "Pay with credit card", + "pdf": { + "summary": "PDF Label", "value": { - "paymentMethod": "CREDIT_CARD", - "paymentDetails": { - "last4": "4242", - "brand": "visa" - } + "id": "550e8400-e29b-41d4-a716-446655440000", + "shipmentId": "123e4567-e89b-12d3-a456-426614174000", + "format": "PDF", + "url": "https://api.sh.example.com/v1/labels/550e8400-e29b-41d4-a716-446655440000", + "createdAt": "2025-01-09T12:00:00Z", + "expiresAt": "2025-01-16T12:00:00Z" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Duties paid successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "amount": { - "type": "number", - "format": "float" - }, - "currency": { - "type": "string" - }, - "receipt": { - "type": "string", - "format": "uri" - } + }, + "png": { + "summary": "PNG Label", + "value": { + "id": "661f9511-f3ac-52e5-b827-557766551111", + "shipmentId": "123e4567-e89b-12d3-a456-426614174000", + "format": "PNG", + "url": "https://api.sh.example.com/v1/labels/661f9511-f3ac-52e5-b827-557766551111", + "createdAt": "2025-01-09T12:05:00Z", + "expiresAt": "2025-01-16T12:05:00Z" } }, - "example": { - "amount": 125.5, - "currency": "GBP", - "receipt": "https://api.sh.example.com/v1/receipts/duty_123456.pdf" + "zpl": { + "summary": "ZPL Label (Thermal Printer)", + "description": "Label in ZPL format for direct thermal printing", + "value": { + "id": "772f0622-g4bd-63f6-c938-668877662222", + "shipmentId": "123e4567-e89b-12d3-a456-426614174000", + "format": "ZPL", + "url": "https://api.sh.example.com/v1/labels/772f0622-g4bd-63f6-c938-668877662222", + "createdAt": "2025-01-09T12:10:00Z", + "expiresAt": "2025-01-16T12:10:00Z" + } } } - } - } - } - } - }, - "/shipments/{shipmentId}/label": { - "get": { - "tags": ["Documentation"], - "summary": "Get shipping label", - "description": "Get the shipping label for a shipment in various formats. Supports both JSON and XML responses. XML format follows the EDIFACT D96A standard for shipping label interchange, while JSON is provided for modern API integrations.", - "operationId": "getLabel", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "format", - "in": "query", - "schema": { - "type": "string", - "enum": ["PDF", "PNG", "ZPL"] }, - "description": "Label format" - }, - { - "name": "Accept", - "in": "header", - "schema": { - "type": "string", - "enum": ["application/json", "application/xml"], - "default": "application/json" - }, - "description": "Response format. Use application/xml for EDI-compliant responses following EDIFACT D96A standard." - } - ], - "responses": { - "200": { - "description": "Label generated successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["shipmentId", "format"], - "properties": { - "id": { - "type": "string", - "format": "uuid", - "readOnly": true - }, - "shipmentId": { - "type": "string", - "format": "uuid" - }, - "format": { - "type": "string", - "enum": ["PDF", "PNG", "ZPL"] - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "createdAt": { - "type": "string", - "format": "date-time", - "readOnly": true - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "readOnly": true - } - } - }, - "examples": { - "pdf": { - "summary": "PDF Label", - "value": { - "id": "550e8400-e29b-41d4-a716-446655440000", - "shipmentId": "123e4567-e89b-12d3-a456-426614174000", - "format": "PDF", - "url": "https://api.sh.example.com/v1/labels/550e8400-e29b-41d4-a716-446655440000", - "createdAt": "2025-01-09T12:00:00Z", - "expiresAt": "2025-01-16T12:00:00Z" - } + "application/xml": { + "schema": { + "type": "object", + "required": ["shipmentId", "format"], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "readOnly": true }, - "png": { - "summary": "PNG Label", - "value": { - "id": "661f9511-f3ac-52e5-b827-557766551111", - "shipmentId": "123e4567-e89b-12d3-a456-426614174000", - "format": "PNG", - "url": "https://api.sh.example.com/v1/labels/661f9511-f3ac-52e5-b827-557766551111", - "createdAt": "2025-01-09T12:05:00Z", - "expiresAt": "2025-01-16T12:05:00Z" - } + "shipmentId": { + "type": "string", + "format": "uuid" }, - "zpl": { - "summary": "ZPL Label (Thermal Printer)", - "description": "Label in ZPL format for direct thermal printing", - "value": { - "id": "772f0622-g4bd-63f6-c938-668877662222", - "shipmentId": "123e4567-e89b-12d3-a456-426614174000", - "format": "ZPL", - "url": "https://api.sh.example.com/v1/labels/772f0622-g4bd-63f6-c938-668877662222", - "createdAt": "2025-01-09T12:10:00Z", - "expiresAt": "2025-01-16T12:10:00Z" - } + "format": { + "type": "string", + "enum": ["PDF", "PNG", "ZPL"] + }, + "url": { + "type": "string", + "format": "uri", + "readOnly": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "expiresAt": { + "type": "string", + "format": "date-time", + "readOnly": true } } }, - "application/xml": { - "schema": { - "type": "object", - "required": ["shipmentId", "format"], - "properties": { - "id": { - "type": "string", - "format": "uuid", - "readOnly": true - }, - "shipmentId": { - "type": "string", - "format": "uuid" - }, - "format": { - "type": "string", - "enum": ["PDF", "PNG", "ZPL"] - }, - "url": { - "type": "string", - "format": "uri", - "readOnly": true - }, - "createdAt": { - "type": "string", - "format": "date-time", - "readOnly": true - }, - "expiresAt": { - "type": "string", - "format": "date-time", - "readOnly": true - } - } + "examples": { + "pdf": { + "summary": "PDF Label (EDIFACT D96A)", + "description": "Label response in EDIFACT D96A XML format for EDI compliance", + "value": "\n\n \n UNOC\n SHIPHAPPENS\n CARRIER123\n 2025010912000\n \n \n" }, - "examples": { - "pdf": { - "summary": "PDF Label (EDIFACT D96A)", - "description": "Label response in EDIFACT D96A XML format for EDI compliance", - "value": "\n\n \n UNOC\n SHIPHAPPENS\n CARRIER123\n 2025010912000\n \n \n" - }, - "png": { - "summary": "PNG Label (EDIFACT D96A)", - "description": "Label response in EDIFACT D96A XML format for EDI compliance", - "value": "\n\n \n UNOC\n SHIPHAPPENS\n CARRIER123\n 2025010912050\n \n \n" - }, - "zpl": { - "summary": "ZPL Label (EDIFACT D96A)", - "description": "Label response in EDIFACT D96A XML format for EDI compliance, suitable for thermal printers", - "value": "\n\n \n UNOC\n SHIPHAPPENS\n CARRIER123\n 2025010912100\n \n \n" - } + "png": { + "summary": "PNG Label (EDIFACT D96A)", + "description": "Label response in EDIFACT D96A XML format for EDI compliance", + "value": "\n\n \n UNOC\n SHIPHAPPENS\n CARRIER123\n 2025010912050\n \n \n" + }, + "zpl": { + "summary": "ZPL Label (EDIFACT D96A)", + "description": "Label response in EDIFACT D96A XML format for EDI compliance, suitable for thermal printers", + "value": "\n\n \n UNOC\n SHIPHAPPENS\n CARRIER123\n 2025010912100\n \n \n" } } } - }, - "404": { - "description": "Shipment not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "code": "NOT_FOUND", - "message": "Shipment not found" - } + } + }, + "404": { + "description": "Shipment not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "code": "NOT_FOUND", + "message": "Shipment not found" } } } } } - }, - "/shipments/{shipmentId}/documents/commercial-invoice": { - "get": { - "tags": ["Documentation"], - "summary": "Get commercial invoice", - "description": "Generate a commercial invoice for an international shipment", - "operationId": "getCommercialInvoice", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - }, - { - "name": "format", - "in": "query", - "schema": { - "type": "string", - "enum": ["PDF", "DOCX"] - } + } + }, + "/shipments/{shipmentId}/documents/commercial-invoice": { + "get": { + "tags": ["Documentation"], + "summary": "Get commercial invoice", + "description": "Generate a commercial invoice for an international shipment", + "operationId": "getCommercialInvoice", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } - ], - "responses": { - "200": { - "description": "Commercial invoice generated successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri" - }, - "expiresAt": { - "type": "string", - "format": "date-time" - } + }, + { + "name": "format", + "in": "query", + "schema": { + "type": "string", + "enum": ["PDF", "DOCX"] + } + } + ], + "responses": { + "200": { + "description": "Commercial invoice generated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "expiresAt": { + "type": "string", + "format": "date-time" } - }, - "example": { - "url": "https://api.sh.example.com/v1/documents/invoice_123456.pdf", - "expiresAt": "2025-01-16T12:00:00Z" } + }, + "example": { + "url": "https://api.sh.example.com/v1/documents/invoice_123456.pdf", + "expiresAt": "2025-01-16T12:00:00Z" } } } } } - }, - "/shipments/{shipmentId}/events": { - "get": { - "tags": ["Tracking & Notifications"], - "summary": "Get shipment tracking events", - "description": "Retrieve detailed tracking events for a shipment", - "operationId": "getTrackingEvents", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } + } + }, + "/shipments/{shipmentId}/events": { + "get": { + "tags": ["Tracking & Notifications"], + "summary": "Get shipment tracking events", + "description": "Retrieve detailed tracking events for a shipment", + "operationId": "getTrackingEvents", + "parameters": [ + { + "name": "shipmentId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" } - ], - "responses": { - "200": { - "description": "Tracking events retrieved successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shipmentId": { - "type": "string", - "format": "uuid" - }, - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "timestamp": { - "type": "string", - "format": "date-time" - }, - "status": { - "type": "string" - }, - "location": { - "type": "object", - "properties": { - "city": { - "type": "string" - }, - "state": { - "type": "string" - }, - "country": { - "type": "string" - }, - "coordinates": { - "type": "object", - "properties": { - "latitude": { - "type": "number" - }, - "longitude": { - "type": "number" - } + } + ], + "responses": { + "200": { + "description": "Tracking events retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "shipmentId": { + "type": "string", + "format": "uuid" + }, + "events": { + "type": "array", + "items": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string" + }, + "location": { + "type": "object", + "properties": { + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "country": { + "type": "string" + }, + "coordinates": { + "type": "object", + "properties": { + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" } } } - }, - "description": { - "type": "string" - }, - "details": { - "type": "object", - "additionalProperties": true } + }, + "description": { + "type": "string" + }, + "details": { + "type": "object", + "additionalProperties": true } } } } - }, - "examples": { - "domestic_delivery": { - "summary": "Successful domestic delivery", - "value": { - "shipmentId": "123e4567-e89b-12d3-a456-426614174000", - "events": [ - { - "timestamp": "2025-01-09T16:30:00Z", - "status": "DELIVERED", - "location": { - "city": "Shiptown", - "state": "ST", - "country": "US", - "coordinates": { - "latitude": 37.7749, - "longitude": -122.4194 - } - }, - "description": "Package delivered to recipient", - "details": { - "signedBy": "John Smith", - "deliveryLocation": "Front Door" + } + }, + "examples": { + "domestic_delivery": { + "summary": "Successful domestic delivery", + "value": { + "shipmentId": "123e4567-e89b-12d3-a456-426614174000", + "events": [ + { + "timestamp": "2025-01-09T16:30:00Z", + "status": "DELIVERED", + "location": { + "city": "Shiptown", + "state": "ST", + "country": "US", + "coordinates": { + "latitude": 37.7749, + "longitude": -122.4194 } }, - { - "timestamp": "2025-01-09T09:15:00Z", - "status": "OUT_FOR_DELIVERY", - "location": { - "city": "Shiptown", - "state": "ST", - "country": "US", - "coordinates": { - "latitude": 37.7749, - "longitude": -122.4194 - } - }, - "description": "Package is out for delivery", - "details": { - "vehicleId": "VAN123", - "estimatedDelivery": "2025-01-09T17:00:00Z" + "description": "Package delivered to recipient", + "details": { + "signedBy": "John Smith", + "deliveryLocation": "Front Door" + } + }, + { + "timestamp": "2025-01-09T09:15:00Z", + "status": "OUT_FOR_DELIVERY", + "location": { + "city": "Shiptown", + "state": "ST", + "country": "US", + "coordinates": { + "latitude": 37.7749, + "longitude": -122.4194 } }, - { - "timestamp": "2025-01-09T02:30:00Z", - "status": "ARRIVED_AT_FACILITY", - "location": { - "city": "Shiptown", - "state": "ST", - "country": "US", - "coordinates": { - "latitude": 37.7749, - "longitude": -122.4194 - } - }, - "description": "Package arrived at local facility", - "details": { - "facilityId": "ST123" + "description": "Package is out for delivery", + "details": { + "vehicleId": "VAN123", + "estimatedDelivery": "2025-01-09T17:00:00Z" + } + }, + { + "timestamp": "2025-01-09T02:30:00Z", + "status": "ARRIVED_AT_FACILITY", + "location": { + "city": "Shiptown", + "state": "ST", + "country": "US", + "coordinates": { + "latitude": 37.7749, + "longitude": -122.4194 } + }, + "description": "Package arrived at local facility", + "details": { + "facilityId": "ST123" } - ] - } - }, - "international_exception": { - "summary": "International shipment with customs delay", - "value": { - "shipmentId": "987fcdeb-a654-3210-9876-543210987654", - "events": [ - { - "timestamp": "2025-01-09T14:20:00Z", - "status": "EXCEPTION", - "location": { - "city": "London", - "country": "GB", - "coordinates": { - "latitude": 51.5074, - "longitude": -0.1278 - } - }, - "description": "Customs clearance delay", - "details": { - "reason": "Additional documentation required", - "requiredDocs": [ - "Commercial Invoice", - "Certificate of Origin" - ], - "contactEmail": "customs@sh.example.com" + } + ] + } + }, + "international_exception": { + "summary": "International shipment with customs delay", + "value": { + "shipmentId": "987fcdeb-a654-3210-9876-543210987654", + "events": [ + { + "timestamp": "2025-01-09T14:20:00Z", + "status": "EXCEPTION", + "location": { + "city": "London", + "country": "GB", + "coordinates": { + "latitude": 51.5074, + "longitude": -0.1278 } }, - { - "timestamp": "2025-01-09T08:45:00Z", - "status": "ARRIVED_AT_CUSTOMS", - "location": { - "city": "London", - "country": "GB", - "coordinates": { - "latitude": 51.5074, - "longitude": -0.1278 - } - }, - "description": "Package arrived at customs", - "details": { - "customsOffice": "LHR1", - "declarationNumber": "GB123456789" + "description": "Customs clearance delay", + "details": { + "reason": "Additional documentation required", + "requiredDocs": [ + "Commercial Invoice", + "Certificate of Origin" + ], + "contactEmail": "customs@sh.example.com" + } + }, + { + "timestamp": "2025-01-09T08:45:00Z", + "status": "ARRIVED_AT_CUSTOMS", + "location": { + "city": "London", + "country": "GB", + "coordinates": { + "latitude": 51.5074, + "longitude": -0.1278 } }, - { - "timestamp": "2025-01-08T22:15:00Z", - "status": "DEPARTED", - "location": { - "city": "Los Angeles", - "state": "CA", - "country": "US", - "coordinates": { - "latitude": 34.0522, - "longitude": -118.2437 - } - }, - "description": "Package departed origin facility", - "details": { - "flightNumber": "BA282", - "destination": "LHR" + "description": "Package arrived at customs", + "details": { + "customsOffice": "LHR1", + "declarationNumber": "GB123456789" + } + }, + { + "timestamp": "2025-01-08T22:15:00Z", + "status": "DEPARTED", + "location": { + "city": "Los Angeles", + "state": "CA", + "country": "US", + "coordinates": { + "latitude": 34.0522, + "longitude": -118.2437 } + }, + "description": "Package departed origin facility", + "details": { + "flightNumber": "BA282", + "destination": "LHR" } - ] - } + } + ] } } } @@ -1578,76 +1577,60 @@ } } } - }, - "/shipments/{shipmentId}/notifications": { - "post": { - "tags": ["Tracking & Notifications"], - "summary": "Set up notifications", - "description": "Configure notification preferences for shipment status updates", - "operationId": "setupNotifications", - "parameters": [ - { - "name": "shipmentId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid" - } - } - ], - "requestBody": { + } + }, + "/shipments/{shipmentId}/notifications": { + "post": { + "tags": ["Tracking & Notifications"], + "summary": "Set up notifications", + "description": "Configure notification preferences for shipment status updates", + "operationId": "setupNotifications", + "parameters": [ + { + "name": "shipmentId", + "in": "path", "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "array", - "items": { - "type": "string", - "format": "email" - } - }, - "sms": { - "type": "array", - "items": { - "type": "string", - "pattern": "^\\+[1-9]\\d{1,14}$" - } - }, - "webhooks": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - } - }, - "events": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "PICKUP_SCHEDULED", - "IN_TRANSIT", - "OUT_FOR_DELIVERY", - "DELIVERED", - "EXCEPTION" - ] - } + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "array", + "items": { + "type": "string", + "format": "email" } - } - }, - "examples": { - "all_channels": { - "summary": "Notifications on all channels", - "value": { - "email": ["recipient@example.com", "sender@example.com"], - "sms": ["+14155552671"], - "webhooks": ["https://example.com/webhook"], - "events": [ + }, + "sms": { + "type": "array", + "items": { + "type": "string", + "pattern": "^\\+[1-9]\\d{1,14}$" + } + }, + "webhooks": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ "PICKUP_SCHEDULED", + "IN_TRANSIT", "OUT_FOR_DELIVERY", "DELIVERED", "EXCEPTION" @@ -1655,203 +1638,219 @@ } } } + }, + "examples": { + "all_channels": { + "summary": "Notifications on all channels", + "value": { + "email": ["recipient@example.com", "sender@example.com"], + "sms": ["+14155552671"], + "webhooks": ["https://example.com/webhook"], + "events": [ + "PICKUP_SCHEDULED", + "OUT_FOR_DELIVERY", + "DELIVERED", + "EXCEPTION" + ] + } + } } } - }, - "responses": { - "200": { - "description": "Notification preferences updated successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["ACTIVE"] - }, - "channels": { - "type": "array", - "items": { - "type": "string" - } + } + }, + "responses": { + "200": { + "description": "Notification preferences updated successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["ACTIVE"] + }, + "channels": { + "type": "array", + "items": { + "type": "string" } } - }, - "example": { - "status": "ACTIVE", - "channels": ["email", "sms", "webhook"] } + }, + "example": { + "status": "ACTIVE", + "channels": ["email", "sms", "webhook"] } } } } } - }, - "/routes/{originFacilityId}/{destinationFacilityId}/{serviceLevel}/estimate": { - "get": { - "tags": ["Route Planning"], - "summary": "Get estimated delivery time", - "description": "Get estimated delivery time between two facilities for a specific service level", - "operationId": "getRouteEstimate", - "parameters": [ - { - "name": "originFacilityId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "pattern": "^[A-Z]{3}[0-9]{1}$", - "example": "LAX1" - }, - "description": "ID of the origin facility" + } + }, + "/routes/{originFacilityId}/{destinationFacilityId}/{serviceLevel}/estimate": { + "get": { + "tags": ["Route Planning"], + "summary": "Get estimated delivery time", + "description": "Get estimated delivery time between two facilities for a specific service level", + "operationId": "getRouteEstimate", + "parameters": [ + { + "name": "originFacilityId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Z]{3}[0-9]{1}$", + "example": "LAX1" }, - { - "name": "destinationFacilityId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "pattern": "^[A-Z]{3}[0-9]{1}$", - "example": "JFK1" - }, - "description": "ID of the destination facility" + "description": "ID of the origin facility" + }, + { + "name": "destinationFacilityId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[A-Z]{3}[0-9]{1}$", + "example": "JFK1" }, - { - "name": "serviceLevel", - "in": "path", - "required": true, - "schema": { - "type": "string", - "enum": ["ECONOMY", "STANDARD", "EXPRESS", "SAME_DAY"] - }, - "description": "Service level for the route" - } - ], - "responses": { - "200": { - "description": "Route estimate retrieved successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "estimatedDeliveryTime": { - "type": "object", - "properties": { - "min": { - "type": "integer", - "description": "Minimum delivery time" - }, - "max": { - "type": "integer", - "description": "Maximum delivery time" - }, - "unit": { + "description": "ID of the destination facility" + }, + { + "name": "serviceLevel", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": ["ECONOMY", "STANDARD", "EXPRESS", "SAME_DAY"] + }, + "description": "Service level for the route" + } + ], + "responses": { + "200": { + "description": "Route estimate retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "estimatedDeliveryTime": { + "type": "object", + "properties": { + "min": { + "type": "integer", + "description": "Minimum delivery time" + }, + "max": { + "type": "integer", + "description": "Maximum delivery time" + }, + "unit": { + "type": "string", + "enum": ["HOURS", "DAYS"] + } + } + }, + "distance": { + "type": "object", + "properties": { + "value": { + "type": "number", + "format": "float" + }, + "unit": { + "type": "string", + "enum": ["KM", "MI"] + } + } + }, + "route": { + "type": "object", + "properties": { + "transitHubs": { + "type": "array", + "items": { + "type": "string" + } + }, + "transportModes": { + "type": "array", + "items": { "type": "string", - "enum": ["HOURS", "DAYS"] + "enum": ["AIR", "GROUND", "SEA"] } } + } + } + } + }, + "examples": { + "domestic_ground": { + "summary": "Domestic ground shipping estimate", + "value": { + "estimatedDeliveryTime": { + "min": 2, + "max": 3, + "unit": "DAYS" }, "distance": { - "type": "object", - "properties": { - "value": { - "type": "number", - "format": "float" - }, - "unit": { - "type": "string", - "enum": ["KM", "MI"] - } - } + "value": 2789.4, + "unit": "MI" }, "route": { - "type": "object", - "properties": { - "transitHubs": { - "type": "array", - "items": { - "type": "string" - } - }, - "transportModes": { - "type": "array", - "items": { - "type": "string", - "enum": ["AIR", "GROUND", "SEA"] - } - } - } + "transitHubs": ["DEN1", "CHI1"], + "transportModes": ["GROUND"] } } }, - "examples": { - "domestic_ground": { - "summary": "Domestic ground shipping estimate", - "value": { - "estimatedDeliveryTime": { - "min": 2, - "max": 3, - "unit": "DAYS" - }, - "distance": { - "value": 2789.4, - "unit": "MI" - }, - "route": { - "transitHubs": ["DEN1", "CHI1"], - "transportModes": ["GROUND"] - } - } - }, - "express_air": { - "summary": "Express air shipping estimate", - "value": { - "estimatedDeliveryTime": { - "min": 8, - "max": 12, - "unit": "HOURS" - }, - "distance": { - "value": 2789.4, - "unit": "MI" - }, - "route": { - "transitHubs": [], - "transportModes": ["AIR"] - } + "express_air": { + "summary": "Express air shipping estimate", + "value": { + "estimatedDeliveryTime": { + "min": 8, + "max": 12, + "unit": "HOURS" + }, + "distance": { + "value": 2789.4, + "unit": "MI" + }, + "route": { + "transitHubs": [], + "transportModes": ["AIR"] } } } } } - }, - "400": { - "description": "Invalid input", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "code": "INVALID_FACILITY", - "message": "Invalid facility ID format" - } + } + }, + "400": { + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "code": "INVALID_FACILITY", + "message": "Invalid facility ID format" } } - }, - "404": { - "description": "Route not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "code": "ROUTE_NOT_FOUND", - "message": "No route found between specified facilities" - } + } + }, + "404": { + "description": "Route not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "code": "ROUTE_NOT_FOUND", + "message": "No route found between specified facilities" } } } @@ -1859,4 +1858,5 @@ } } } - } \ No newline at end of file + } +} diff --git a/examples/ship-happens/schema/webhooks.json b/examples/ship-happens/schema/webhooks.json index a6959779..223df7b3 100644 --- a/examples/ship-happens/schema/webhooks.json +++ b/examples/ship-happens/schema/webhooks.json @@ -1,204 +1,185 @@ { "openapi": "3.0.3", "info": { - "title": "SWebhook API", - "description": "This API allows you to register webhooks to receive real-time updates about your shipments.\n\n## Authentication\nAll endpoints require a valid API key passed in the `X-API-Key` header.\n\n## Webhook Events\nThe following events are available for subscription:\n- `shipment.created`\n- `shipment.in_transit`\n- `shipment.delivered`\n- `shipment.exception`\n", - "version": "1.0.0", - "contact": { - "name": "Ship Happens API Support", - "email": "api@sh.example.com", - "url": "https://developers.sh.example.com" - } + "title": "SWebhook API", + "description": "This API allows you to register webhooks to receive real-time updates about your shipments.\n\n## Authentication\nAll endpoints require a valid API key passed in the `X-API-Key` header.\n\n## Webhook Events\nThe following events are available for subscription:\n- `shipment.created`\n- `shipment.in_transit`\n- `shipment.delivered`\n- `shipment.exception`\n", + "version": "1.0.0", + "contact": { + "name": "Ship Happens API Support", + "email": "api@sh.example.com", + "url": "https://developers.sh.example.com" + } }, "servers": [ - { - "url": "https://api.sh.example.com/v1", - "description": "Production environment" - }, - { - "url": "https://api.staging.sh.example.com/v1", - "description": "Staging environment" - }, - { - "url": "https://api.dev.sh.example.com/v1", - "description": "Development environment" - } + { + "url": "https://api.sh.example.com/v1", + "description": "Production environment" + }, + { + "url": "https://api.staging.sh.example.com/v1", + "description": "Staging environment" + }, + { + "url": "https://api.dev.sh.example.com/v1", + "description": "Development environment" + } ], "security": [ - { - "ApiKeyAuth": [] - } + { + "ApiKeyAuth": [] + } ], "components": { - "securitySchemes": { - "ApiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "X-API-Key" + "securitySchemes": { + "ApiKeyAuth": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "schemas": { + "Webhook": { + "type": "object", + "required": ["url", "events"], + "properties": { + "id": { + "type": "string", + "format": "uuid", + "readOnly": true + }, + "url": { + "type": "string", + "format": "uri", + "description": "The URL where webhook events will be sent" + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "shipment.created", + "shipment.in_transit", + "shipment.delivered", + "shipment.exception" + ] + } + }, + "active": { + "type": "boolean", + "default": true + }, + "createdAt": { + "type": "string", + "format": "date-time", + "readOnly": true + }, + "secret": { + "type": "string", + "writeOnly": true, + "description": "Secret used to sign webhook payloads" } + } }, - "schemas": { - "Webhook": { - "type": "object", - "required": [ - "url", - "events" - ], - "properties": { - "id": { - "type": "string", - "format": "uuid", - "readOnly": true - }, - "url": { - "type": "string", - "format": "uri", - "description": "The URL where webhook events will be sent" - }, - "events": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "shipment.created", - "shipment.in_transit", - "shipment.delivered", - "shipment.exception" - ] - } - }, - "active": { - "type": "boolean", - "default": true - }, - "createdAt": { - "type": "string", - "format": "date-time", - "readOnly": true - }, - "secret": { - "type": "string", - "writeOnly": true, - "description": "Secret used to sign webhook payloads" - } - } + "Error": { + "type": "object", + "required": ["code", "message"], + "properties": { + "code": { + "type": "string" }, - "Error": { - "type": "object", - "required": [ - "code", - "message" - ], - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - } - } + "message": { + "type": "string" } + } } + } }, "paths": { - "/webhooks": { - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Register a new webhook", - "description": "Registers a new webhook endpoint to receive shipment updates.\n\nA secret will be generated and returned in the response. This secret should be used to verify the authenticity of webhook payloads.\n", - "operationId": "registerWebhook", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Webhook" - }, - "example": { - "url": "https://api.myapp.com/webhooks/shipping", - "events": [ - "shipment.created", - "shipment.delivered" - ] - } - } - } + "/webhooks": { + "post": { + "tags": ["Webhooks"], + "summary": "Register a new webhook", + "description": "Registers a new webhook endpoint to receive shipment updates.\n\nA secret will be generated and returned in the response. This secret should be used to verify the authenticity of webhook payloads.\n", + "operationId": "registerWebhook", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Webhook" }, - "responses": { - "201": { - "description": "Webhook registered successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Webhook" - }, - "example": { - "id": "abcdef12-3456-789a-bcde-f0123456789a", - "url": "https://api.myapp.com/webhooks/shipping", - "events": [ - "shipment.created", - "shipment.delivered" - ], - "active": true, - "createdAt": "2025-01-09T12:00:00Z", - "secret": "whsec_abcdef123456789" - } - } - } - }, - "400": { - "description": "Invalid input", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Error" - }, - "example": { - "code": "INVALID_INPUT", - "message": "Invalid webhook URL provided" - } - } - } - } + "example": { + "url": "https://api.myapp.com/webhooks/shipping", + "events": ["shipment.created", "shipment.delivered"] } + } + } + }, + "responses": { + "201": { + "description": "Webhook registered successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Webhook" + }, + "example": { + "id": "abcdef12-3456-789a-bcde-f0123456789a", + "url": "https://api.myapp.com/webhooks/shipping", + "events": ["shipment.created", "shipment.delivered"], + "active": true, + "createdAt": "2025-01-09T12:00:00Z", + "secret": "whsec_abcdef123456789" + } + } + } }, - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List all webhooks", - "description": "Returns a list of all registered webhooks", - "operationId": "listWebhooks", - "responses": { - "200": { - "description": "List of webhooks retrieved successfully", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Webhook" - } - }, - "example": [ - { - "id": "abcdef12-3456-789a-bcde-f0123456789a", - "url": "https://api.myapp.com/webhooks/shipping", - "events": [ - "shipment.created", - "shipment.delivered" - ], - "active": true, - "createdAt": "2025-01-09T12:00:00Z" - } - ] - } - } + "400": { + "description": "Invalid input", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + }, + "example": { + "code": "INVALID_INPUT", + "message": "Invalid webhook URL provided" + } + } + } + } + } + }, + "get": { + "tags": ["Webhooks"], + "summary": "List all webhooks", + "description": "Returns a list of all registered webhooks", + "operationId": "listWebhooks", + "responses": { + "200": { + "description": "List of webhooks retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Webhook" + } + }, + "example": [ + { + "id": "abcdef12-3456-789a-bcde-f0123456789a", + "url": "https://api.myapp.com/webhooks/shipping", + "events": ["shipment.created", "shipment.delivered"], + "active": true, + "createdAt": "2025-01-09T12:00:00Z" } + ] } + } } + } } + } } -} \ No newline at end of file +} diff --git a/examples/with-zuplo/docs/.env.local b/examples/with-zuplo/docs/.env.local new file mode 100644 index 00000000..1ee437f0 --- /dev/null +++ b/examples/with-zuplo/docs/.env.local @@ -0,0 +1 @@ +ZUPLO=1 diff --git a/examples/with-zuplo/.gitignore b/examples/with-zuplo/docs/.gitignore similarity index 100% rename from examples/with-zuplo/.gitignore rename to examples/with-zuplo/docs/.gitignore diff --git a/examples/with-zuplo/dev-portal.json b/examples/with-zuplo/docs/dev-portal.json similarity index 91% rename from examples/with-zuplo/dev-portal.json rename to examples/with-zuplo/docs/dev-portal.json index 35a061c5..a306d6a4 100644 --- a/examples/with-zuplo/dev-portal.json +++ b/examples/with-zuplo/docs/dev-portal.json @@ -16,7 +16,7 @@ "redirects": [{ "from": "/", "to": "/introduction" }], "apis": { "type": "file", - "input": "config/routes.oas.json", + "input": "../config/routes.oas.json", "navigationId": "api" }, "docs": { diff --git a/examples/with-zuplo/env.example b/examples/with-zuplo/docs/env.example similarity index 100% rename from examples/with-zuplo/env.example rename to examples/with-zuplo/docs/env.example diff --git a/examples/with-zuplo/package.json b/examples/with-zuplo/docs/package.json similarity index 100% rename from examples/with-zuplo/package.json rename to examples/with-zuplo/docs/package.json diff --git a/examples/with-zuplo/pages/introduction.mdx b/examples/with-zuplo/docs/pages/introduction.mdx similarity index 100% rename from examples/with-zuplo/pages/introduction.mdx rename to examples/with-zuplo/docs/pages/introduction.mdx diff --git a/examples/with-zuplo/project.json b/examples/with-zuplo/docs/project.json similarity index 69% rename from examples/with-zuplo/project.json rename to examples/with-zuplo/docs/project.json index bebddcc7..6a74a3af 100644 --- a/examples/with-zuplo/project.json +++ b/examples/with-zuplo/docs/project.json @@ -1,6 +1,6 @@ { "name": "with-zuplo", - "$schema": "../node_modules/nx/schemas/nx-schema.json", + "$schema": "../../../node_modules/nx/schemas/nx-schema.json", "targets": { "build": { "options": { diff --git a/examples/with-zuplo/tsconfig.json b/examples/with-zuplo/docs/tsconfig.json similarity index 100% rename from examples/with-zuplo/tsconfig.json rename to examples/with-zuplo/docs/tsconfig.json diff --git a/packages/zudoku/src/app/main.css b/packages/zudoku/src/app/main.css index cda93487..ee6ce1ab 100644 --- a/packages/zudoku/src/app/main.css +++ b/packages/zudoku/src/app/main.css @@ -238,57 +238,57 @@ } /* Theme */ - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 95%; - --input: 240 5.9% 90%; - --ring: 240 5.9% 10%; - --radius: 0.75rem; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - } - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - --chart-1: 220 70% 50%; + @layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 95%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0.75rem; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; diff --git a/packages/zudoku/src/config/loader.ts b/packages/zudoku/src/config/loader.ts index 4014c2b4..3d0aa7eb 100644 --- a/packages/zudoku/src/config/loader.ts +++ b/packages/zudoku/src/config/loader.ts @@ -3,7 +3,7 @@ import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { RollupOutput, RollupWatcher } from "rollup"; import { tsImport } from "tsx/esm/api"; -import withZuplo from "../zuplo/with-zuplo.js"; +import { withZuplo } from "../zuplo/with-zuplo.js"; import { ConfigWithMeta } from "./common.js"; import { CommonConfig, validateCommonConfig } from "./validators/common.js"; import { validateConfig } from "./validators/validate.js"; diff --git a/packages/zudoku/src/config/validators/common.ts b/packages/zudoku/src/config/validators/common.ts index 069e2fd0..4db55e7f 100644 --- a/packages/zudoku/src/config/validators/common.ts +++ b/packages/zudoku/src/config/validators/common.ts @@ -316,6 +316,7 @@ export const CommonConfigSchema = z.object({ apiKeys: ApiKeysSchema, redirects: z.array(Redirect), sitemap: SiteMapSchema, + isZuplo: z.boolean().optional(), }); export const refine = ( diff --git a/packages/zudoku/src/lib/components/index.ts b/packages/zudoku/src/lib/components/index.ts index c52f98f5..7db6df15 100644 --- a/packages/zudoku/src/lib/components/index.ts +++ b/packages/zudoku/src/lib/components/index.ts @@ -6,14 +6,14 @@ import { RouterError as RouterErrorImport } from "../errors/RouterError.js"; import { ServerError as ServerErrorImport } from "../errors/ServerError.js"; import { Button as ButtonImport } from "../ui/Button.js"; import { Callout as CalloutImport } from "../ui/Callout.js"; -import { Spinner as SpinnerImport } from "./Spinner.js"; -import { Markdown as MarkdownImport } from "./Markdown.js"; import { Bootstrap as BootstrapImport, BootstrapStatic as BootstrapStaticImport, } from "./Bootstrap.js"; import { ClientOnly as ClientOnlyImport } from "./ClientOnly.js"; import { Layout as LayoutImport } from "./Layout.js"; +import { Markdown as MarkdownImport } from "./Markdown.js"; +import { Spinner as SpinnerImport } from "./Spinner.js"; import { Zudoku as ZudokuImport } from "./Zudoku.js"; import { useZudoku as useZudokuImport } from "./context/ZudokuContext.js"; export const useMDXComponents = /*@__PURE__*/ useMDXComponentsImport; diff --git a/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.test.ts b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.test.ts index f5d03710..75bc1171 100644 --- a/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.test.ts +++ b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.test.ts @@ -4,28 +4,34 @@ import { removeExtensions } from "./removeExtensions.js"; const baseDoc = { openapi: "3.1.0", "x-root-ext": "remove me", + "x-zuplo-ext": "remove me too", info: { title: "Test API", version: "1.0.0", "x-info-ext": "remove me", + "x-zuplo-info": "remove me too", }, paths: { "/test": { "x-path-ext": "remove me", + "x-zuplo-path": "remove me too", parameters: [ { name: "param1", in: "query", schema: { type: "string" }, "x-param-ext": "remove me", + "x-zuplo-param": "remove me too", }, ], get: { "x-operation-ext": "remove me", + "x-zuplo-route": "remove me too", responses: { "200": { description: "OK", "x-response-ext": "remove me", + "x-zuplo-response": "remove me too", }, }, parameters: [ @@ -34,6 +40,7 @@ const baseDoc = { in: "header", schema: { type: "string" }, "x-op-param-ext": "remove me", + "x-zuplo-param": "remove me too", }, ], }, @@ -43,6 +50,7 @@ const baseDoc = { { name: "example", "x-tag-ext": "remove me", + "x-zuplo-tag": "remove me too", }, ], components: { @@ -52,6 +60,7 @@ const baseDoc = { name: "api_key", in: "header", "x-security-ext": "remove me", + "x-zuplo-security": "remove me too", }, }, }, @@ -141,4 +150,53 @@ describe("removeExtensions", () => { expect(processed).toEqual(docWithoutExtensions); }); + + it("removes extensions based on shouldRemove callback", () => { + const processed = removeExtensions({ + shouldRemove: (key) => key.startsWith("x-zuplo"), + })(baseDoc); + + // Should remove x-zuplo extensions + const removedExtensions = [ + "x-zuplo-ext", + "info.x-zuplo-info", + "paths./test.x-zuplo-path", + "paths./test.parameters.0.x-zuplo-param", + "paths./test.get.x-zuplo-route", + "paths./test.get.responses.200.x-zuplo-response", + "paths./test.get.parameters.0.x-zuplo-param", + "tags.0.x-zuplo-tag", + "components.securitySchemes.ApiKeyAuth.x-zuplo-security", + ]; + + // Should keep other x- extensions + const keptExtensions = [ + "x-root-ext", + "info.x-info-ext", + "paths./test.x-path-ext", + "paths./test.parameters.0.x-param-ext", + "paths./test.get.x-operation-ext", + "paths./test.get.responses.200.x-response-ext", + "paths./test.get.parameters.0.x-op-param-ext", + "tags.0.x-tag-ext", + "components.securitySchemes.ApiKeyAuth.x-security-ext", + ]; + + removedExtensions.forEach((ext) => { + expect(processed).not.toHaveProperty(ext.split(".")); + }); + + keptExtensions.forEach((ext) => { + expect(processed).toHaveProperty(ext.split(".")); + }); + + // Assert that non-x- fields remain unchanged + expect(processed).toHaveProperty("openapi", "3.1.0"); + expect(processed).toHaveProperty("info.title", "Test API"); + expect(processed).toHaveProperty( + "paths./test.get.responses.200.description", + "OK", + ); + expect(processed).toHaveProperty("tags.0.name", "example"); + }); }); diff --git a/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.ts b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.ts index a3d215ff..97362a91 100644 --- a/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.ts +++ b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeExtensions.ts @@ -2,21 +2,24 @@ import { type RecordAny, traverse } from "./traverse.js"; interface RemoveExtensionsOptions { keys?: string[]; + shouldRemove?: (key: string) => boolean; } // Remove all `x-` prefixed key/value pairs, or filter by names if provided export const removeExtensions = - ({ keys }: RemoveExtensionsOptions = {}) => + ({ keys, shouldRemove }: RemoveExtensionsOptions = {}) => (doc: RecordAny): RecordAny => traverse(doc, (spec) => { const result: RecordAny = {}; for (const [key, value] of Object.entries(spec)) { const isExtension = key.startsWith("x-"); - const shouldRemove = - isExtension && (keys === undefined || keys.includes(key)); + const shouldBeRemoved = + isExtension && + (keys === undefined || keys.includes(key)) && + (!shouldRemove || shouldRemove(key)); - if (shouldRemove) continue; + if (shouldBeRemoved) continue; result[key] = value; } diff --git a/packages/zudoku/src/lib/plugins/openapi/post-processors/removeParameters.test.ts b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeParameters.test.ts new file mode 100644 index 00000000..aff7ae1c --- /dev/null +++ b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeParameters.test.ts @@ -0,0 +1,148 @@ +import { type OpenAPIV3_1 } from "openapi-types"; +import { describe, expect, it } from "vitest"; +import { removeParameters } from "./removeParameters.js"; + +const baseDoc: OpenAPIV3_1.Document = { + openapi: "3.1.0", + info: { + title: "Test API", + version: "1.0.0", + }, + components: { + parameters: { + commonParam: { + name: "commonParam", + in: "query", + schema: { type: "string" }, + }, + headerParam: { + name: "headerParam", + in: "header", + schema: { type: "string" }, + }, + }, + }, + paths: { + "/test": { + parameters: [ + { + name: "pathParam", + in: "path", + schema: { type: "string" }, + required: true, + }, + { + name: "pathHeader", + in: "header", + schema: { type: "string" }, + required: true, + }, + ], + get: { + parameters: [ + { + name: "opParam", + in: "query", + schema: { type: "string" }, + required: true, + }, + { + name: "opHeader", + in: "header", + schema: { type: "string" }, + required: true, + }, + ], + responses: { + "200": { + description: "OK", + }, + }, + }, + }, + }, +}; + +describe("removeParameters", () => { + it("removes parameters by name", () => { + const processed = removeParameters({ + names: ["pathParam", "opParam"], + })(baseDoc); + + expect(processed.paths["/test"].parameters).toHaveLength(1); + expect(processed.paths["/test"].parameters[0].name).toBe("pathHeader"); + expect(processed.paths["/test"].get.parameters).toHaveLength(1); + expect(processed.paths["/test"].get.parameters[0].name).toBe("opHeader"); + }); + + it("removes parameters by location", () => { + const processed = removeParameters({ + in: ["header"], + })(baseDoc); + + expect(processed.paths["/test"].parameters).toHaveLength(1); + expect(processed.paths["/test"].parameters[0].in).toBe("path"); + expect(processed.paths["/test"].get.parameters).toHaveLength(1); + expect(processed.paths["/test"].get.parameters[0].in).toBe("query"); + }); + + it("removes parameters using shouldRemove callback", () => { + const processed = removeParameters({ + shouldRemove: ({ parameter }) => + parameter.in === "header" && parameter.name.includes("op"), + })(baseDoc); + + expect(processed.paths["/test"].parameters).toHaveLength(2); + expect(processed.paths["/test"].get.parameters).toHaveLength(1); + expect(processed.paths["/test"].get.parameters[0].name).toBe("opParam"); + }); + + it("combines multiple removal criteria", () => { + const processed = removeParameters({ + in: ["query", "header"], + shouldRemove: ({ parameter }) => parameter.name === "pathHeader", + })(baseDoc); + + expect(processed.paths["/test"].parameters).toHaveLength(1); + expect(processed.paths["/test"].parameters[0].name).toBe("pathParam"); + expect(processed.paths["/test"].get.parameters).toHaveLength(0); + }); + + it("handles missing parameters arrays", () => { + const docWithoutParams = { + openapi: "3.1.0", + paths: { + "/test": { + get: { + summary: "Test endpoint", + }, + }, + }, + }; + + const processed = removeParameters({ + names: ["someParam"], + })(docWithoutParams); + + expect(processed).toEqual(docWithoutParams); + }); + + it("preserves non-parameter properties", () => { + const processed = removeParameters({ + names: ["globalParam"], + })(baseDoc); + + expect(processed.openapi).toBe("3.1.0"); + expect(processed.paths["/test"].get).toBeDefined(); + }); + + it("removes parameters from components", () => { + const processed = removeParameters({ + in: ["header"], + })(baseDoc); + + expect(Object.keys(processed.components.parameters)).toHaveLength(1); + expect(processed.components.parameters.commonParam).toBeDefined(); + expect(processed.components.parameters.headerParam).toBeUndefined(); + }); +}); diff --git a/packages/zudoku/src/lib/plugins/openapi/post-processors/removeParameters.ts b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeParameters.ts new file mode 100644 index 00000000..238f1255 --- /dev/null +++ b/packages/zudoku/src/lib/plugins/openapi/post-processors/removeParameters.ts @@ -0,0 +1,101 @@ +import { type RecordAny, traverse } from "./traverse.js"; + +interface RemoveParametersOptions { + // Names of parameters to remove + names?: string[]; + // Specific locations to remove parameters from ('query', 'header', 'path', 'cookie') + in?: string[]; + // Custom filter function + shouldRemove?: ({ parameter }: { parameter: RecordAny }) => boolean; +} + +export const removeParameters = + ({ names, in: locations, shouldRemove }: RemoveParametersOptions = {}) => + (doc: RecordAny): RecordAny => + traverse(doc, (spec) => { + // Helper function to filter parameters + const filterParameters = (parameters: RecordAny[]) => + parameters.filter((p) => { + if (names?.includes(p.name)) return false; + if (locations?.includes(p.in)) return false; + if (shouldRemove?.({ parameter: p })) return false; + return true; + }); + + // Handle components.parameters + if (spec.components?.parameters) { + spec = { + ...spec, + components: { + ...spec.components, + parameters: Object.fromEntries( + Object.entries(spec.components.parameters).filter( + ([_, param]) => { + const p = param as RecordAny; + if (p.$ref) return true; // Skip references + return ( + !names?.includes(p.name) && + !locations?.includes(p.in) && + !shouldRemove?.({ parameter: p }) + ); + }, + ), + ), + }, + }; + } + + // Handle paths + if (spec.paths) { + const updatedPaths: RecordAny = {}; + + for (const [path, pathItem] of Object.entries(spec.paths)) { + if (typeof pathItem !== "object" || pathItem === null) { + updatedPaths[path] = pathItem; + continue; + } + + let updatedPathItem = { ...pathItem }; + + // Handle path-level parameters + if ( + "parameters" in updatedPathItem && + Array.isArray(updatedPathItem.parameters) + ) { + updatedPathItem.parameters = filterParameters( + updatedPathItem.parameters, + ); + } + + // Handle operation-level parameters + for (const method of Object.keys(updatedPathItem)) { + const pathItemWithMethods = updatedPathItem as Record< + string, + RecordAny + >; + + if ( + method === "parameters" || + typeof pathItemWithMethods[method] !== "object" + ) { + continue; + } + + const operation = pathItemWithMethods[method]; + if (Array.isArray(operation.parameters)) { + pathItemWithMethods[method] = { + ...operation, + parameters: filterParameters(operation.parameters), + }; + updatedPathItem = pathItemWithMethods; + } + } + + updatedPaths[path] = updatedPathItem; + } + + spec = { ...spec, paths: updatedPaths }; + } + + return spec; + }); diff --git a/packages/zudoku/src/vite/plugin-api.ts b/packages/zudoku/src/vite/plugin-api.ts index c084045f..d27e6290 100644 --- a/packages/zudoku/src/vite/plugin-api.ts +++ b/packages/zudoku/src/vite/plugin-api.ts @@ -1,5 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { tsImport } from "tsx/esm/api"; import { type Plugin } from "vite"; import yaml from "yaml"; import { type ZudokuPluginOptions } from "../config/config.js"; @@ -21,6 +22,8 @@ const schemaMap = new Map(); async function processSchemas( config: ZudokuPluginOptions, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + zuploProcessors: Array<(schema: any) => Promise> = [], ): Promise> { const tmpDir = path.posix.join( config.rootDir, @@ -39,8 +42,12 @@ async function processSchemas( continue; } - const postProcessors = apiConfig.postProcessors ?? []; - postProcessors.unshift((schema) => upgradeSchema(schema)); + const postProcessors = [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (schema: any) => upgradeSchema(schema), + ...(apiConfig.postProcessors ?? []), + ...zuploProcessors, + ]; const inputs = Array.isArray(apiConfig.input) ? apiConfig.input @@ -89,16 +96,31 @@ async function processSchemas( return processedSchemas; } -const viteApiPlugin = (getConfig: () => ZudokuPluginOptions): Plugin => { +const viteApiPlugin = async ( + getConfig: () => ZudokuPluginOptions, +): Promise => { const virtualModuleId = "virtual:zudoku-api-plugins"; const resolvedVirtualModuleId = "\0" + virtualModuleId; - let processedSchemas: Awaited>; + const initialConfig = getConfig(); + + // Load Zuplo-specific processors if in Zuplo environment + const zuploProcessors = initialConfig.isZuplo + ? await tsImport("../zuplo/with-zuplo-processors.ts", import.meta.url) + .then((m) => m.default(initialConfig.rootDir)) + .catch((e) => { + // eslint-disable-next-line no-console + console.warn("Failed to load Zuplo processors", e); + return []; + }) + : []; + + let processedSchemas: Record; return { name: "zudoku-api-plugins", async buildStart() { - processedSchemas = await processSchemas(getConfig()); + processedSchemas = await processSchemas(getConfig(), zuploProcessors); }, resolveId(id) { if (id === virtualModuleId) { diff --git a/packages/zudoku/src/zuplo/enrich-with-zuplo.ts b/packages/zudoku/src/zuplo/enrich-with-zuplo.ts new file mode 100644 index 00000000..c7d66faf --- /dev/null +++ b/packages/zudoku/src/zuplo/enrich-with-zuplo.ts @@ -0,0 +1,252 @@ +import { OpenAPIV3_1 } from "openapi-types"; +import { RecordAny } from "../lib/util/traverse.js"; +import { + PoliciesConfigFile, + PolicyConfigurationFragment, +} from "./policy-types.js"; + +const API_KEY_REPLACEMENT_STRING = "YOUR_KEY_HERE"; + +const enrichWithApiKeyData = ( + operationObject: RecordAny, + apiKeyPolicies: PolicyConfigurationFragment[], +) => { + if (apiKeyPolicies.length === 0) { + return operationObject; + } + + const firstPolicy = apiKeyPolicies[0]; + const authorizationHeader = + (firstPolicy?.handler.options?.["authHeader"] as string) || "Authorization"; + const authorizationScheme = (firstPolicy?.handler.options?.["authScheme"] ?? + "Bearer") as string; + const authSchemeExample = + authorizationScheme !== "" + ? `${authorizationScheme} ${API_KEY_REPLACEMENT_STRING}` + : API_KEY_REPLACEMENT_STRING; + + // Add API key header parameter + const apiKeyHeader: OpenAPIV3_1.ParameterObject = { + name: authorizationHeader, + in: "header", + required: true, + example: authSchemeExample, + schema: { + type: "string", + }, + description: `The \`${authorizationHeader}\` header is used to authenticate with the API using your API key. Value is of the format \`${authSchemeExample}\`.`, + }; + + const parameters = operationObject.parameters || []; + if ( + !parameters.some((param: RecordAny) => param.name === authorizationHeader) + ) { + operationObject.parameters = [apiKeyHeader, ...parameters]; + } + + // Add security scheme and requirement + const apiSecuritySchemeId = "api_key"; + const apiKeySecurityRequirement = { [apiSecuritySchemeId]: [] }; + + if (!operationObject.security) { + operationObject.security = [apiKeySecurityRequirement]; + } else if ( + !operationObject.security.some((req: RecordAny) => req[apiSecuritySchemeId]) + ) { + operationObject.security = [ + apiKeySecurityRequirement, + ...operationObject.security, + ]; + } + + return operationObject; +}; + +const enrichWithRateLimitData = ( + operationObject: RecordAny, + rateLimitPolicies: PolicyConfigurationFragment[], +) => { + if (rateLimitPolicies.length === 0) { + return operationObject; + } + + const shouldIncludeHeader = rateLimitPolicies.some( + (policy) => policy.handler.options?.headerMode !== "none", + ); + + if (!operationObject.responses) { + operationObject.responses = {}; + } + + if (!operationObject.responses["429"]) { + operationObject.responses["429"] = { + $ref: shouldIncludeHeader + ? "#/components/responses/RateLimitWithRetryAfter" + : "#/components/responses/RateLimitNoRetryAfter", + }; + } + + return operationObject; +}; + +// prettier-ignore +const operations = [ + "get", "put", "post", "delete", + "options", "head", "patch", "trace", +]; + +const rateLimitingResponse: OpenAPIV3_1.ResponseObject = { + description: "Rate Limiting Response", + content: { + "application/json": { + schema: { + type: "object", + required: ["type", "title", "status"], + examples: [ + { + type: "https://httpproblems.com/http-status/429", + title: "Too Many Requests", + status: 429, + instance: "/foo/bar", + }, + ], + properties: { + type: { + type: "string", + example: "https://httpproblems.com/http-status/429", + description: "A URI reference that identifies the problem.", + }, + title: { + type: "string", + example: "Too Many Requests", + description: "A short, human-readable summary of the problem.", + }, + status: { + type: "number", + example: 429, + description: "The HTTP status code.", + }, + instance: { + type: "string", + example: "/foo/bar", + }, + }, + }, + }, + }, +}; + +const rateLimitingResponseWithHeader: OpenAPIV3_1.ResponseObject = { + ...rateLimitingResponse, + headers: { + "retry-after": { + description: "The number of seconds to wait before making a new request.", + schema: { + type: "integer", + example: 60, + }, + }, + }, +}; + +export const enrichWithZuploData = ({ + policiesConfig, +}: { + policiesConfig: PoliciesConfigFile; +}) => { + return (spec: RecordAny) => { + if (!spec.paths) return spec; + + let hasRateLimitPolicies = false; + + for (const [, pathItem] of Object.entries(spec.paths)) { + for (const method of operations) { + const operation = pathItem[method]; + if (!operation["x-zuplo-route"]) continue; + + const inboundPolicies = operation[ + "x-zuplo-route" + ]?.policies?.inbound?.reduce((acc: string[], policyName: string) => { + const policy = policiesConfig.policies?.find( + ({ name }) => name === policyName, + ); + if (!policy) return acc; + + // Handle composite policies + if (policy.handler.export === "CompositeInboundPolicy") { + const childPolicies = policy.handler.options?.policies as + | string[] + | undefined; + return childPolicies ? [...acc, ...childPolicies] : acc; + } + + return [...acc, policyName]; + }, []); + + if (!inboundPolicies) continue; + + // Find API key policies + const apiKeyPolicies = + policiesConfig.policies?.filter( + (policy) => + inboundPolicies.includes(policy.name) && + (policy.handler.export === "ApiAuthKeyInboundPolicy" || + policy.handler.export === "ApiKeyInboundPolicy") && + !policy.handler.options + ?.disableAutomaticallyAddingKeyHeaderToOpenApi, + ) ?? []; + + // Find rate limit policies + const rateLimitPolicies = + policiesConfig.policies?.filter( + (policy) => + inboundPolicies.includes(policy.name) && + (policy.handler.export === "RateLimitInboundPolicy" || + policy.handler.export === "ComplexRateLimitInboundPolicy"), + ) ?? []; + + if (rateLimitPolicies.length > 0) { + hasRateLimitPolicies = true; + } + + // Apply enrichments directly to the operation + pathItem[method] = enrichWithApiKeyData(operation, apiKeyPolicies); + pathItem[method] = enrichWithRateLimitData( + pathItem[method], + rateLimitPolicies, + ); + } + } + + // Add security scheme if we have API key policies + if ( + policiesConfig.policies?.some( + (policy) => + policy.handler.export === "ApiAuthKeyInboundPolicy" || + policy.handler.export === "ApiKeyInboundPolicy", + ) + ) { + if (!spec.components) spec.components = {}; + if (!spec.components.securitySchemes) + spec.components.securitySchemes = {}; + + if (!spec.components.securitySchemes.api_key) { + spec.components.securitySchemes.api_key = { + type: "http", + scheme: "bearer", + }; + } + } + + // Add rate limiting responses only if we found rate limiting policies + if (hasRateLimitPolicies) { + if (!spec.components) spec.components = {}; + if (!spec.components.responses) spec.components.responses = {}; + spec.components.responses.RateLimitNoRetryAfter = rateLimitingResponse; + spec.components.responses.RateLimitWithRetryAfter = + rateLimitingResponseWithHeader; + } + + return spec; + }; +}; diff --git a/packages/zudoku/src/zuplo/env.ts b/packages/zudoku/src/zuplo/env.ts index 4bd9ed96..0db62f63 100644 --- a/packages/zudoku/src/zuplo/env.ts +++ b/packages/zudoku/src/zuplo/env.ts @@ -5,4 +5,8 @@ export const ZuploEnv = { get isZuplo(): boolean { return process.env.ZUPLO === "1"; }, + + get serverUrl(): string | undefined { + return process.env.ZUPLO_SERVER_URL; + }, }; diff --git a/packages/zudoku/src/zuplo/policy-types.ts b/packages/zudoku/src/zuplo/policy-types.ts new file mode 100644 index 00000000..33dcc57a --- /dev/null +++ b/packages/zudoku/src/zuplo/policy-types.ts @@ -0,0 +1,47 @@ +/* eslint-disable */ +/** + * This file was automatically generated by json-schema-to-typescript. + * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, + * and run json-schema-to-typescript to regenerate this file. + */ + +export interface PoliciesConfigFile { + policies?: PolicyConfigurationFragment[]; + corsPolicies?: CorsPolicyConfiguration[]; +} +export interface PolicyConfigurationFragment { + name: string; + policyType: string; + handler: HandlerDefinition; + options?: { + [k: string]: unknown; + }; +} +export interface HandlerDefinition { + module: string; + export: string; + options?: { + [k: string]: unknown; + }; +} +export interface CorsPolicyConfiguration { + name: string; + allowCredentials?: boolean; + maxAge?: number; + allowedOrigins: string[] | string; + allowedMethods?: + | ( + | "GET" + | "HEAD" + | "POST" + | "PUT" + | "DELETE" + | "CONNECT" + | "OPTIONS" + | "TRACE" + | "PATCH" + )[] + | string; + allowedHeaders?: string[] | string; + exposeHeaders?: string[] | string; +} diff --git a/packages/zudoku/src/zuplo/with-zuplo-processors.ts b/packages/zudoku/src/zuplo/with-zuplo-processors.ts new file mode 100644 index 00000000..b401b4fe --- /dev/null +++ b/packages/zudoku/src/zuplo/with-zuplo-processors.ts @@ -0,0 +1,30 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { removeExtensions } from "../lib/plugins/openapi/post-processors/removeExtensions.js"; +import { removeParameters } from "../lib/plugins/openapi/post-processors/removeParameters.js"; +import { removePaths } from "../lib/plugins/openapi/post-processors/removePaths.js"; +import { type RecordAny } from "../lib/util/traverse.js"; +import { enrichWithZuploData } from "./enrich-with-zuplo.js"; +import { ZuploEnv } from "./env.js"; + +export const getProcessors = async (rootDir: string) => { + const policiesConfig = JSON.parse( + await fs.readFile(path.join(rootDir, "../config/policies.json"), "utf-8"), + ); + + return [ + removePaths({ shouldRemove: ({ operation }) => operation["x-internal"] }), + removeParameters({ + shouldRemove: ({ parameter }) => parameter["x-internal"], + }), + enrichWithZuploData({ policiesConfig }), + (spec: RecordAny) => { + const url = ZuploEnv.serverUrl; + if (!url) return spec; + return { ...spec, servers: [{ url }] }; + }, + removeExtensions({ shouldRemove: (key) => key.startsWith("x-zuplo") }), + ]; +}; + +export default getProcessors; diff --git a/packages/zudoku/src/zuplo/with-zuplo.ts b/packages/zudoku/src/zuplo/with-zuplo.ts index 20d826f6..8c4fbde1 100644 --- a/packages/zudoku/src/zuplo/with-zuplo.ts +++ b/packages/zudoku/src/zuplo/with-zuplo.ts @@ -1,32 +1,10 @@ -import { CommonConfig, ZudokuApiConfig } from "../config/validators/common.js"; -import { removeExtensions } from "../lib/plugins/openapi/post-processors/removeExtensions.js"; -import { removePaths } from "../lib/plugins/openapi/post-processors/removePaths.js"; - -function withZuplo(config: TConfig): TConfig { - if (config.apis) { - if (Array.isArray(config.apis)) { - config.apis = config.apis.map(configureApis); - } else { - config.apis = configureApis(config.apis); - } - } - - return config; -} - -function configureApis(config: ZudokuApiConfig): ZudokuApiConfig { - if (config.type === "file") { - config.postProcessors = [ - removeExtensions({ keys: ["x-zuplo-route", "x-zuplo-path"] }), - removePaths({ - // custom filter (method is `true` for all methods) - shouldRemove: ({ operation }) => operation["x-internal"], - }), - ...(config.postProcessors ?? []), - ]; - } - - return config; -} - -export default withZuplo; +import { CommonConfig } from "../config/validators/common.js"; + +export const withZuplo = ( + config: TConfig, +): TConfig => { + return { + ...config, + isZuplo: true, + }; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93188ae3..6d9c0583 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,7 +214,7 @@ importers: specifier: workspace:* version: link:../../packages/zudoku - examples/with-zuplo: + examples/with-zuplo/docs: dependencies: '@mdx-js/react': specifier: 3.0.1 @@ -227,7 +227,7 @@ importers: version: 19.0.0(react@19.0.0) zudoku: specifier: workspace:* - version: link:../../packages/zudoku + version: link:../../../packages/zudoku devDependencies: '@types/node': specifier: ^20 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4d74ea74..6defb46a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,7 @@ packages: - "scripts" - "packages/*" - - "examples/*" + - "examples/**" - "website" - "docs"