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"