diff --git a/.github/workflows/schema-validation.yml b/.github/workflows/schema-validation.yml new file mode 100644 index 0000000000..7d772503ff --- /dev/null +++ b/.github/workflows/schema-validation.yml @@ -0,0 +1,27 @@ +name: JSON Schema Validation + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + schema-validation: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run all tests + run: npm test \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000..45648086f1 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,3 @@ +#!/bin/sh + +npm run precommit \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3dee5bc38f..e579206428 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,6 +42,58 @@ Implementation details can be mentioned as: - Write for an audience implementing the protocol, not using a specific implementation - Keep examples generic and illustrative +## JSON Schema Maintenance + +### Schema-Documentation Consistency + +**GOLDEN RULE**: Documentation and JSON schemas MUST always be synchronized. + +### When to Update Schemas + +Update JSON schemas whenever you: +- Add, remove, or rename any fields in task requests/responses +- Change field types, constraints, or validation rules +- Modify enum values (like status types, delivery types, etc.) +- Add new data models or modify existing core objects +- Change required vs optional field specifications + +### Schema Update Checklist + +When making documentation changes: +1. ✅ Identify affected schemas in `static/schemas/v1/` +2. ✅ Update request schemas (if changing task parameters) +3. ✅ Update response schemas (if changing response structure) +4. ✅ Update core data models (if changing object definitions) +5. ✅ Update enum schemas (if changing allowed values) +6. ✅ Verify cross-references (`$ref` links) are still valid +7. ✅ Test schema validation with example data +8. ✅ Update schema descriptions to match documentation + +### Schema Location Map + +- **Task Requests**: `static/schemas/v1/media-buy/` or `static/schemas/v1/signals/` +- **Core Objects**: `static/schemas/v1/core/` +- **Enums**: `static/schemas/v1/enums/` +- **Registry**: `static/schemas/v1/index.json` + +### Validation Testing + +Always validate schemas work correctly: +```bash +# Use online JSON schema validators or +# Node.js with ajv library to test schemas +# Schemas are accessible locally at http://localhost:3000/schemas/v1/ when running npm run start +``` + +### Local Schema Access + +When running `npm run start`, all JSON schemas are accessible at: +- Schema registry: `http://localhost:3000/schemas/v1/index.json` +- Core schemas: `http://localhost:3000/schemas/v1/core/{name}.json` +- Task schemas: `http://localhost:3000/schemas/v1/media-buy/{task}-{request|response}.json` +- Signal schemas: `http://localhost:3000/schemas/v1/signals/{task}-{request|response}.json` +- Enum schemas: `http://localhost:3000/schemas/v1/enums/{name}.json` + ## Code Standards ### TypeScript/JavaScript @@ -68,6 +120,11 @@ Implementation details can be mentioned as: 2. Update all related documentation when making changes 3. Ensure examples are consistent across docs 4. Remove version numbers while in v1 development +5. **CRITICAL: JSON Schema Synchronization** + - When changing any task parameters, data models, or field definitions in docs, **ALWAYS update the corresponding JSON schemas** in `static/schemas/v1/` + - When updating JSON schemas, **ALWAYS verify the documentation matches** the schema definitions + - Check both request/response schemas AND core data model schemas for affected changes + - Update the schema registry (`static/schemas/v1/index.json`) if adding/removing schemas ### Code Changes 1. Follow existing patterns diff --git a/docs/intro.md b/docs/intro.md index 0052bdf017..d7b991a03d 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -21,6 +21,8 @@ AdCP uses a task-first architecture where core advertising tasks (like creating - **A2A (Agent2Agent Protocol)**: For complex workflows and agent collaboration - **REST API**: Coming soon for traditional integrations +All tasks and data models are defined with JSON schemas for validation and client code generation. + ## Protocol Architecture AdCP operates at multiple layers, providing a clean separation between the business roles, orchestration layer, and technical execution: diff --git a/docs/media-buy/tasks/add_creative_assets.md b/docs/media-buy/tasks/add_creative_assets.md index b9fab11dda..fea2889632 100644 --- a/docs/media-buy/tasks/add_creative_assets.md +++ b/docs/media-buy/tasks/add_creative_assets.md @@ -7,6 +7,10 @@ sidebar_position: 4 Upload creative assets and assign them to packages. This task includes validation, policy review, and format adaptation suggestions. + +**Request Schema**: [`/schemas/v1/media-buy/add-creative-assets-request.json`](/schemas/v1/media-buy/add-creative-assets-request.json) +**Response Schema**: [`/schemas/v1/media-buy/add-creative-assets-response.json`](/schemas/v1/media-buy/add-creative-assets-response.json) + ## Request Parameters | Parameter | Type | Required | Description | diff --git a/docs/media-buy/tasks/create_media_buy.md b/docs/media-buy/tasks/create_media_buy.md index 5e2949f57f..78309f10ce 100644 --- a/docs/media-buy/tasks/create_media_buy.md +++ b/docs/media-buy/tasks/create_media_buy.md @@ -7,6 +7,10 @@ sidebar_position: 3 Create a media buy from selected packages. This task handles the complete workflow including validation, approval if needed, and campaign creation. + +**Request Schema**: [`/schemas/v1/media-buy/create-media-buy-request.json`](/schemas/v1/media-buy/create-media-buy-request.json) +**Response Schema**: [`/schemas/v1/media-buy/create-media-buy-response.json`](/schemas/v1/media-buy/create-media-buy-response.json) + ## Request Parameters | Parameter | Type | Required | Description | diff --git a/docs/media-buy/tasks/get_media_buy_delivery.md b/docs/media-buy/tasks/get_media_buy_delivery.md index 0a6414cfcc..8bdfc5195d 100644 --- a/docs/media-buy/tasks/get_media_buy_delivery.md +++ b/docs/media-buy/tasks/get_media_buy_delivery.md @@ -7,6 +7,10 @@ sidebar_position: 6 Retrieve comprehensive delivery metrics and performance data for reporting. + +**Request Schema**: [`/schemas/v1/media-buy/get-media-buy-delivery-request.json`](/schemas/v1/media-buy/get-media-buy-delivery-request.json) +**Response Schema**: [`/schemas/v1/media-buy/get-media-buy-delivery-response.json`](/schemas/v1/media-buy/get-media-buy-delivery-response.json) + ## Request Parameters | Parameter | Type | Required | Description | diff --git a/docs/media-buy/tasks/get_products.md b/docs/media-buy/tasks/get_products.md index 6705751f58..b80eb9a5be 100644 --- a/docs/media-buy/tasks/get_products.md +++ b/docs/media-buy/tasks/get_products.md @@ -4,6 +4,10 @@ sidebar_position: 1 --- # get_products Discover available advertising products based on campaign requirements, using natural language briefs or structured filters. + +**Request Schema**: [`/schemas/v1/media-buy/get-products-request.json`](/schemas/v1/media-buy/get-products-request.json) +**Response Schema**: [`/schemas/v1/media-buy/get-products-response.json`](/schemas/v1/media-buy/get-products-response.json) + ## Request Parameters | Parameter | Type | Required | Description | diff --git a/docs/media-buy/tasks/list_creative_formats.md b/docs/media-buy/tasks/list_creative_formats.md index 427fb810d3..98b608d985 100644 --- a/docs/media-buy/tasks/list_creative_formats.md +++ b/docs/media-buy/tasks/list_creative_formats.md @@ -7,6 +7,10 @@ sidebar_position: 2 Discover all supported creative formats in the system. + +**Request Schema**: [`/schemas/v1/media-buy/list-creative-formats-request.json`](/schemas/v1/media-buy/list-creative-formats-request.json) +**Response Schema**: [`/schemas/v1/media-buy/list-creative-formats-response.json`](/schemas/v1/media-buy/list-creative-formats-response.json) + ## Request Parameters | Parameter | Type | Required | Description | diff --git a/docs/media-buy/tasks/update_media_buy.md b/docs/media-buy/tasks/update_media_buy.md index fa4e9ed5d8..47635c4cc0 100644 --- a/docs/media-buy/tasks/update_media_buy.md +++ b/docs/media-buy/tasks/update_media_buy.md @@ -7,6 +7,10 @@ sidebar_position: 7 Update campaign and package settings. This task supports partial updates and handles any required approvals. + +**Request Schema**: [`/schemas/v1/media-buy/update-media-buy-request.json`](/schemas/v1/media-buy/update-media-buy-request.json) +**Response Schema**: [`/schemas/v1/media-buy/update-media-buy-response.json`](/schemas/v1/media-buy/update-media-buy-response.json) + ## Request Parameters | Parameter | Type | Required | Description | diff --git a/docs/reference/data-models.md b/docs/reference/data-models.md index e122f14850..d906676fee 100644 --- a/docs/reference/data-models.md +++ b/docs/reference/data-models.md @@ -11,6 +11,8 @@ Core data structures used throughout AdCP. Represents available advertising inventory. +**JSON Schema**: [`/schemas/v1/core/product.json`](/schemas/v1/core/product.json) + ```typescript interface Product { product_id: string; @@ -34,6 +36,8 @@ interface Product { Represents a purchased advertising campaign. +**JSON Schema**: [`/schemas/v1/core/media-buy.json`](/schemas/v1/core/media-buy.json) + ```typescript interface MediaBuy { media_buy_id: string; @@ -51,6 +55,8 @@ interface MediaBuy { A specific product within a media buy (line item). +**JSON Schema**: [`/schemas/v1/core/package.json`](/schemas/v1/core/package.json) + ```typescript interface Package { package_id: string; @@ -67,6 +73,8 @@ interface Package { Uploaded creative content. +**JSON Schema**: [`/schemas/v1/core/creative-asset.json`](/schemas/v1/core/creative-asset.json) + ```typescript interface CreativeAsset { creative_id: string; @@ -85,6 +93,8 @@ interface CreativeAsset { Audience targeting criteria. +**JSON Schema**: [`/schemas/v1/core/targeting.json`](/schemas/v1/core/targeting.json) + ```typescript interface Targeting { geo_country_any_of?: string[]; @@ -102,6 +112,8 @@ interface Targeting { Standard response structure (MCP). +**JSON Schema**: [`/schemas/v1/core/response.json`](/schemas/v1/core/response.json) + ```typescript interface Response { message: string; // Human-readable summary @@ -116,6 +128,8 @@ interface Response { Standard error structure. +**JSON Schema**: [`/schemas/v1/core/error.json`](/schemas/v1/core/error.json) + ```typescript interface Error { code: string; @@ -129,15 +143,27 @@ interface Error { ## Common Enums ```typescript -// Delivery Types +// Delivery Types - Schema: /schemas/v1/enums/delivery-type.json type DeliveryType = 'guaranteed' | 'non_guaranteed'; -// Media Buy Status +// Media Buy Status - Schema: /schemas/v1/enums/media-buy-status.json type MediaBuyStatus = 'pending_activation' | 'active' | 'paused' | 'completed'; -// Creative Status +// Creative Status - Schema: /schemas/v1/enums/creative-status.json type CreativeStatus = 'processing' | 'approved' | 'rejected' | 'pending_review'; -// Pacing +// Pacing - Schema: /schemas/v1/enums/pacing.json type Pacing = 'even' | 'asap' | 'front_loaded'; -``` \ No newline at end of file +``` + +**Additional Enum Schemas**: +- [`delivery-type.json`](/schemas/v1/enums/delivery-type.json) - guaranteed vs non_guaranteed +- [`media-buy-status.json`](/schemas/v1/enums/media-buy-status.json) - Media buy lifecycle status +- [`package-status.json`](/schemas/v1/enums/package-status.json) - Package lifecycle status +- [`creative-status.json`](/schemas/v1/enums/creative-status.json) - Creative review status +- [`pacing.json`](/schemas/v1/enums/pacing.json) - Budget pacing strategies +- [`frequency-cap-scope.json`](/schemas/v1/enums/frequency-cap-scope.json) - Frequency cap scope + +## JSON Schema Registry + +View all available schemas: [`/schemas/v1/index.json`](/schemas/v1/index.json) \ No newline at end of file diff --git a/docs/signals/tasks/activate_signal.md b/docs/signals/tasks/activate_signal.md index 9fbb8885ba..58564ab022 100644 --- a/docs/signals/tasks/activate_signal.md +++ b/docs/signals/tasks/activate_signal.md @@ -7,6 +7,9 @@ title: activate_signal **Task**: Activate a signal for use on a specific platform/account. +**Request Schema**: [`/schemas/v1/signals/activate-signal-request.json`](/schemas/v1/signals/activate-signal-request.json) +**Response Schema**: [`/schemas/v1/signals/activate-signal-response.json`](/schemas/v1/signals/activate-signal-response.json) + The `activate_signal` task handles the entire activation lifecycle, including: - Initiating the activation request - Monitoring activation progress diff --git a/docs/signals/tasks/get_signals.md b/docs/signals/tasks/get_signals.md index 15915a931c..86c1519647 100644 --- a/docs/signals/tasks/get_signals.md +++ b/docs/signals/tasks/get_signals.md @@ -7,6 +7,9 @@ title: get_signals **Task**: Discover signals based on description, with details about where they are deployed. +**Request Schema**: [`/schemas/v1/signals/get-signals-request.json`](/schemas/v1/signals/get-signals-request.json) +**Response Schema**: [`/schemas/v1/signals/get-signals-response.json`](/schemas/v1/signals/get-signals-response.json) + The `get_signals` task returns both signal metadata and real-time deployment status across platforms, allowing agents to understand availability and guide the activation process. ## Request Parameters diff --git a/package-lock.json b/package-lock.json index fdfb9077d5..1fb3dd2451 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,9 @@ "@docusaurus/module-type-aliases": "3.8.1", "@docusaurus/tsconfig": "3.8.1", "@docusaurus/types": "3.8.1", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "husky": "^9.0.11", "typescript": "~5.6.2" }, "engines": { @@ -5661,9 +5664,10 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -10146,6 +10150,22 @@ "node": ">=10.17.0" } }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/hyperdyperid": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", @@ -16834,6 +16854,23 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/search-insights": { "version": "2.17.3", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", diff --git a/package.json b/package.json index 854c5c94e0..b5880dd5e8 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,12 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc" + "typecheck": "tsc", + "test:schemas": "node tests/schema-validation.test.js", + "test:examples": "node tests/example-validation-simple.test.js", + "test": "npm run test:schemas && npm run test:examples && npm run typecheck", + "precommit": "npm test", + "prepare": "husky" }, "dependencies": { "@docusaurus/core": "3.8.1", @@ -29,7 +34,10 @@ "@docusaurus/module-type-aliases": "3.8.1", "@docusaurus/tsconfig": "3.8.1", "@docusaurus/types": "3.8.1", - "typescript": "~5.6.2" + "typescript": "~5.6.2", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "husky": "^9.0.11" }, "browserslist": { "production": [ diff --git a/static/schemas/README.md b/static/schemas/README.md new file mode 100644 index 0000000000..90d98eb2a8 --- /dev/null +++ b/static/schemas/README.md @@ -0,0 +1,374 @@ +# AdCP JSON Schemas + +This directory contains JSON Schema definitions for all AdCP (Advertising Context Protocol) objects and task request/response structures. + +## Overview + +The schemas are organized to provide comprehensive validation for: +- **Core Data Models**: Fundamental objects used throughout the protocol +- **Task Schemas**: Request and response structures for each protocol task +- **Enums**: Standardized enumerated values +- **Signal Protocol**: Schemas specific to the signals extension + +## Directory Structure + +``` +schemas/ +├── v1/ # Version 1 schemas +│ ├── core/ # Core data models +│ │ ├── product.json +│ │ ├── media-buy.json +│ │ ├── package.json +│ │ ├── creative-asset.json +│ │ ├── targeting.json +│ │ ├── budget.json +│ │ ├── frequency-cap.json +│ │ ├── format.json +│ │ ├── measurement.json +│ │ ├── creative-policy.json +│ │ ├── error.json +│ │ └── response.json +│ ├── media-buy/ # Media buy task schemas +│ │ ├── get-products-request.json +│ │ ├── get-products-response.json +│ │ ├── create-media-buy-request.json +│ │ ├── create-media-buy-response.json +│ │ ├── add-creative-assets-request.json +│ │ ├── add-creative-assets-response.json +│ │ ├── update-media-buy-request.json +│ │ ├── update-media-buy-response.json +│ │ ├── get-media-buy-delivery-request.json +│ │ ├── get-media-buy-delivery-response.json +│ │ ├── list-creative-formats-request.json +│ │ └── list-creative-formats-response.json +│ ├── signals/ # Signals protocol schemas +│ │ ├── get-signals-request.json +│ │ ├── get-signals-response.json +│ │ ├── activate-signal-request.json +│ │ └── activate-signal-response.json +│ ├── enums/ # Enum definitions +│ │ ├── delivery-type.json +│ │ ├── media-buy-status.json +│ │ ├── creative-status.json +│ │ ├── package-status.json +│ │ ├── pacing.json +│ │ └── frequency-cap-scope.json +│ └── index.json # Schema registry +├── asset-types-v1.json # Creative asset types (existing) +├── creative-formats-v1.json # Creative format definitions (existing) +└── README.md # This file +``` + +## Usage + +### Local Development + +When running the Docusaurus development server locally (`npm run start`), all schemas are accessible at: +- **Schema Registry**: `http://localhost:3000/schemas/v1/index.json` +- **Core Schemas**: `http://localhost:3000/schemas/v1/core/{schema-name}.json` +- **Task Schemas**: `http://localhost:3000/schemas/v1/media-buy/{task-name}-{request|response}.json` +- **Signals Schemas**: `http://localhost:3000/schemas/v1/signals/{task-name}-{request|response}.json` +- **Enum Schemas**: `http://localhost:3000/schemas/v1/enums/{enum-name}.json` + +### Schema Registry + +The `v1/index.json` file serves as the main registry for all schemas, providing: +- Schema URLs and references +- Descriptions for each schema +- Usage examples +- Categorization by protocol area + +### Validation Examples + +#### JavaScript (Node.js) + +```javascript +const Ajv = require('ajv'); +const ajv = new Ajv(); + +// Load and validate a product +const productSchema = require('./schemas/v1/core/product.json'); +const validateProduct = ajv.compile(productSchema); + +const product = { + "product_id": "ctv_sports_premium", + "name": "CTV Sports Premium", + "description": "Premium CTV inventory on sports content", + "formats": [{"format_id": "video_16x9_30s", "name": "30-second video"}], + "delivery_type": "guaranteed", + "is_fixed_price": true +}; + +const isValid = validateProduct(product); +if (!isValid) { + console.log(validateProduct.errors); +} +``` + +#### Python + +```python +import jsonschema +import json + +# Load schema +with open('schemas/v1/core/product.json') as f: + schema = json.load(f) + +# Validate data +product = { + "product_id": "ctv_sports_premium", + "name": "CTV Sports Premium", + "description": "Premium CTV inventory on sports content", + "formats": [{"format_id": "video_16x9_30s", "name": "30-second video"}], + "delivery_type": "guaranteed", + "is_fixed_price": True +} + +try: + jsonschema.validate(product, schema) + print("Valid!") +except jsonschema.ValidationError as e: + print(f"Validation error: {e.message}") +``` + +#### Java + +```java +import com.github.fge.jsonschema.main.JsonSchema; +import com.github.fge.jsonschema.main.JsonSchemaFactory; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +// Load schema +ObjectMapper mapper = new ObjectMapper(); +JsonNode schemaNode = mapper.readTree(new File("schemas/v1/core/product.json")); +JsonSchemaFactory factory = JsonSchemaFactory.byDefault(); +JsonSchema schema = factory.getJsonSchema(schemaNode); + +// Validate data +JsonNode data = mapper.readTree(jsonString); +ProcessingReport report = schema.validate(data); +``` + +## Schema Standards + +All schemas follow these conventions: + +### JSON Schema Draft 07 + +All schemas use JSON Schema Draft 07 specification. + +### Schema IDs + +Each schema has a unique `$id` following the pattern: +``` +https://adcp.dev/schemas/v1/{category}/{name}.json +``` + +### Cross-References + +Schemas reference each other using `$ref` to maintain consistency and avoid duplication. + +### Descriptions + +All properties include comprehensive descriptions explaining their purpose and usage. + +### Validation Rules + +- String patterns for IDs and codes +- Minimum/maximum constraints for numeric values +- Required field specifications +- Additional property restrictions + +## Protocol Coverage + +### Media Buy Protocol + +All media buy tasks are covered with complete request/response schemas: +- `get_products` - Product discovery +- `list_creative_formats` - Format discovery +- `create_media_buy` - Campaign creation +- `add_creative_assets` - Creative upload +- `update_media_buy` - Campaign updates +- `get_media_buy_delivery` - Performance reporting + +### Signals Protocol + +Signal-related tasks have dedicated schemas: +- `get_signals` - Signal discovery +- `activate_signal` - Signal activation + +### Core Data Models + +All fundamental protocol objects are defined: +- Products, Media Buys, Packages +- Creative Assets and Formats +- Targeting and Budget structures +- Measurement and Policy objects + +## Versioning + +Schemas are versioned using semantic versioning in the directory structure: +- `v1/` - Version 1.x schemas +- `v2/` - Version 2.x schemas (future) + +Major version changes indicate breaking changes to the schema structure. + +## Integration + +### Client Libraries + +Use these schemas to generate strongly-typed client libraries in various languages: +- OpenAPI Generator +- quicktype +- Language-specific code generation tools + +### API Documentation + +Reference schemas in API documentation to provide: +- Interactive validation +- Type information +- Field descriptions +- Example data structures + +### Testing + +Use schemas to validate: +- API test fixtures +- Example data in documentation +- Integration test payloads +- Mock data generation + +## Testing + +### Automated Testing + +The schema implementation includes comprehensive testing to ensure accuracy and consistency: + +#### Schema Validation Tests +```bash +# Test all schemas are syntactically valid +npm run test:schemas + +# Individual schema validation +node tests/schema-validation.test.js +``` + +This validates: +- All 36 schemas are syntactically correct JSON Schema Draft-07 +- Cross-references (`$ref`) resolve properly +- Schema registry is consistent with actual files +- Required fields and constraints are properly defined + +#### Example Data Validation Tests +```bash +# Test example data against schemas +npm run test:examples + +# Individual example validation +node tests/example-validation-simple.test.js +``` + +This validates: +- Example data from documentation matches schemas +- Request/response structures are correctly defined +- Core data models validate properly + +#### Complete Test Suite +```bash +# Run all tests including TypeScript checks +npm test +``` + +### Pre-commit Validation + +A pre-commit hook automatically ensures schema integrity on every commit: + +```bash +# Pre-commit hook runs automatically, or test manually via: +npm run precommit +``` + +This runs: +1. Schema validation tests (6 tests) +2. Example data validation tests (7 tests) +3. TypeScript type checking + +### Manual Testing + +#### Test Schema Loading Locally +```bash +# Start development server +npm run start + +# Visit schema URLs in browser: +# http://localhost:3000/schemas/v1/index.json +# http://localhost:3000/schemas/v1/core/product.json +# etc. +``` + +#### Test with External Validators +```bash +# Online JSON Schema validators: +# - https://www.jsonschemavalidator.net/ +# - https://jsonschemalint.com/ + +# Load schema from local server when testing +``` + +### Troubleshooting + +Common issues and solutions: + +#### Schema Reference Errors +``` +Error: can't resolve reference /schemas/v1/enums/pacing.json +``` +**Solution**: Ensure all referenced schemas exist and paths are correct. + +#### Duplicate Schema IDs +``` +Error: schema with key or id already exists +``` +**Solution**: Check for duplicate `$id` values across schemas. + +#### Validation Failures +``` +Schema validation failed for: {data} +``` +**Solution**: Compare data structure against schema requirements, check required fields. + +## Contributing + +When updating schemas: + +1. **Follow existing naming conventions** +2. **Maintain backward compatibility** within major versions +3. **Update the schema registry** (`index.json`) +4. **Run all tests** before committing: + ```bash + npm test + ``` +5. **Update documentation** to match schema changes +6. **Add validation examples** for new schemas +7. **Test locally** by starting dev server and accessing schema URLs + +### Schema-Documentation Consistency + +**CRITICAL**: Documentation and JSON schemas MUST always be synchronized. + +When making changes: +- ✅ Update documentation first +- ✅ Update corresponding schemas +- ✅ Run validation tests +- ✅ Test example data +- ✅ Update schema registry if needed +- ✅ Run pre-commit checks + +## Support + +For questions about the schemas or validation issues: +- Check the AdCP documentation +- Review the schema registry for available schemas +- Validate your data structure against the appropriate schema \ No newline at end of file diff --git a/static/schemas/v1/core/budget.json b/static/schemas/v1/core/budget.json new file mode 100644 index 0000000000..8a0edb19b4 --- /dev/null +++ b/static/schemas/v1/core/budget.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/budget.json", + "title": "Budget", + "description": "Budget configuration for a media buy or package", + "type": "object", + "properties": { + "total": { + "type": "number", + "description": "Total budget amount", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$", + "examples": ["USD", "EUR", "GBP"] + }, + "daily_cap": { + "type": ["number", "null"], + "description": "Daily budget cap (null for no limit)", + "minimum": 0 + }, + "pacing": { + "$ref": "/schemas/v1/enums/pacing.json" + } + }, + "required": ["total", "currency"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/creative-asset.json b/static/schemas/v1/core/creative-asset.json new file mode 100644 index 0000000000..f548f9fddb --- /dev/null +++ b/static/schemas/v1/core/creative-asset.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/creative-asset.json", + "title": "Creative Asset", + "description": "Uploaded creative content", + "type": "object", + "properties": { + "creative_id": { + "type": "string", + "description": "Unique identifier for the creative" + }, + "name": { + "type": "string", + "description": "Human-readable creative name" + }, + "format": { + "type": "string", + "description": "Creative format type (e.g., video, audio, display)" + }, + "media_url": { + "type": "string", + "format": "uri", + "description": "URL of the creative file" + }, + "click_url": { + "type": "string", + "format": "uri", + "description": "Landing page URL for the creative" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL of the creative content" + }, + "duration": { + "type": "number", + "description": "Duration in milliseconds (for video/audio)", + "minimum": 0 + }, + "width": { + "type": "number", + "description": "Width in pixels (for video/display)", + "minimum": 0 + }, + "height": { + "type": "number", + "description": "Height in pixels (for video/display)", + "minimum": 0 + }, + "status": { + "$ref": "/schemas/v1/enums/creative-status.json" + }, + "platform_id": { + "type": "string", + "description": "Platform-specific ID assigned to the creative" + }, + "review_feedback": { + "type": "string", + "description": "Feedback from platform review (if any)" + }, + "compliance": { + "type": "object", + "description": "Compliance review status", + "properties": { + "status": { + "type": "string", + "description": "Compliance status" + }, + "issues": { + "type": "array", + "description": "Array of compliance issues", + "items": { + "type": "string" + } + } + }, + "required": ["status"], + "additionalProperties": false + }, + "package_assignments": { + "type": "array", + "description": "Package IDs or buyer_refs to assign this creative to", + "items": { + "type": "string" + } + }, + "assets": { + "type": "array", + "description": "For multi-asset formats like carousels", + "items": { + "$ref": "/schemas/v1/core/sub-asset.json" + } + } + }, + "required": ["creative_id", "name", "format"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/creative-assignment.json b/static/schemas/v1/core/creative-assignment.json new file mode 100644 index 0000000000..cd3c50d563 --- /dev/null +++ b/static/schemas/v1/core/creative-assignment.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/creative-assignment.json", + "title": "Creative Assignment", + "description": "Assignment of a creative asset to a package", + "type": "object", + "properties": { + "creative_id": { + "type": "string", + "description": "Unique identifier for the creative" + }, + "weight": { + "type": "number", + "description": "Delivery weight for this creative", + "minimum": 0, + "maximum": 100 + } + }, + "required": ["creative_id"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/creative-policy.json b/static/schemas/v1/core/creative-policy.json new file mode 100644 index 0000000000..f161a81801 --- /dev/null +++ b/static/schemas/v1/core/creative-policy.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/creative-policy.json", + "title": "Creative Policy", + "description": "Creative requirements and restrictions for a product", + "type": "object", + "properties": { + "co_branding": { + "type": "string", + "description": "Co-branding requirement", + "enum": ["required", "optional", "none"] + }, + "landing_page": { + "type": "string", + "description": "Landing page requirements", + "enum": ["any", "retailer_site_only", "must_include_retailer"] + }, + "templates_available": { + "type": "boolean", + "description": "Whether creative templates are provided" + } + }, + "required": ["co_branding", "landing_page", "templates_available"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/error.json b/static/schemas/v1/core/error.json new file mode 100644 index 0000000000..3167728c69 --- /dev/null +++ b/static/schemas/v1/core/error.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/error.json", + "title": "Error", + "description": "Standard error structure", + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Error code for programmatic handling" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "field": { + "type": "string", + "description": "Field associated with the error" + }, + "suggestion": { + "type": "string", + "description": "Suggested fix for the error" + }, + "details": { + "description": "Additional error details" + } + }, + "required": ["code", "message"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/format.json b/static/schemas/v1/core/format.json new file mode 100644 index 0000000000..bf98e45c1a --- /dev/null +++ b/static/schemas/v1/core/format.json @@ -0,0 +1,62 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/format.json", + "title": "Format", + "description": "Represents a creative format with its requirements", + "type": "object", + "properties": { + "format_id": { + "type": "string", + "description": "Unique identifier for the format" + }, + "name": { + "type": "string", + "description": "Human-readable format name" + }, + "type": { + "type": "string", + "description": "Format type (e.g., audio, video, display)", + "enum": ["audio", "video", "display"] + }, + "is_standard": { + "type": "boolean", + "description": "Whether this follows IAB standards" + }, + "iab_specification": { + "type": "string", + "description": "Name of the IAB specification (if applicable)" + }, + "requirements": { + "type": "object", + "description": "Format-specific requirements (varies by format type)", + "additionalProperties": true + }, + "assets_required": { + "type": "array", + "description": "Array of required assets for composite formats", + "items": { + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "description": "Type of asset required" + }, + "quantity": { + "type": "integer", + "description": "Number of assets of this type required", + "minimum": 1 + }, + "requirements": { + "type": "object", + "description": "Specific requirements for this asset type", + "additionalProperties": true + } + }, + "required": ["asset_type", "quantity"], + "additionalProperties": false + } + } + }, + "required": ["format_id", "name"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/frequency-cap.json b/static/schemas/v1/core/frequency-cap.json new file mode 100644 index 0000000000..e493b75a04 --- /dev/null +++ b/static/schemas/v1/core/frequency-cap.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/frequency-cap.json", + "title": "Frequency Cap", + "description": "Frequency capping settings", + "type": "object", + "properties": { + "suppress_minutes": { + "type": "number", + "description": "Minutes to suppress after impression", + "minimum": 0 + }, + "scope": { + "$ref": "/schemas/v1/enums/frequency-cap-scope.json" + } + }, + "required": ["suppress_minutes"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/measurement.json b/static/schemas/v1/core/measurement.json new file mode 100644 index 0000000000..2785bc177a --- /dev/null +++ b/static/schemas/v1/core/measurement.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/measurement.json", + "title": "Measurement", + "description": "Measurement capabilities included with a product", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of measurement", + "examples": ["incremental_sales_lift", "brand_lift", "foot_traffic"] + }, + "attribution": { + "type": "string", + "description": "Attribution methodology", + "examples": ["deterministic_purchase", "probabilistic"] + }, + "window": { + "type": "string", + "description": "Attribution window", + "examples": ["30_days", "7_days"] + }, + "reporting": { + "type": "string", + "description": "Reporting frequency and format", + "examples": ["weekly_dashboard", "real_time_api"] + } + }, + "required": ["type", "attribution", "reporting"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/media-buy.json b/static/schemas/v1/core/media-buy.json new file mode 100644 index 0000000000..7163b2e9b0 --- /dev/null +++ b/static/schemas/v1/core/media-buy.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/media-buy.json", + "title": "Media Buy", + "description": "Represents a purchased advertising campaign", + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Publisher's unique identifier for the media buy" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this media buy" + }, + "status": { + "$ref": "/schemas/v1/enums/media-buy-status.json" + }, + "promoted_offering": { + "type": "string", + "description": "Description of advertiser and what is being promoted" + }, + "total_budget": { + "type": "number", + "description": "Total budget amount", + "minimum": 0 + }, + "packages": { + "type": "array", + "description": "Array of packages within this media buy", + "items": { + "$ref": "/schemas/v1/core/package.json" + } + }, + "creative_deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp for creative upload deadline" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Creation timestamp" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "description": "Last update timestamp" + } + }, + "required": [ + "media_buy_id", + "status", + "promoted_offering", + "total_budget", + "packages" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/package.json b/static/schemas/v1/core/package.json new file mode 100644 index 0000000000..e8e4416fb3 --- /dev/null +++ b/static/schemas/v1/core/package.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/package.json", + "title": "Package", + "description": "A specific product within a media buy (line item)", + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Publisher's unique identifier for the package" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this package" + }, + "product_id": { + "type": "string", + "description": "ID of the product this package is based on" + }, + "products": { + "type": "array", + "description": "Array of product IDs to include in this package", + "items": { + "type": "string" + } + }, + "budget": { + "$ref": "/schemas/v1/core/budget.json" + }, + "impressions": { + "type": "number", + "description": "Impression goal for this package", + "minimum": 0 + }, + "targeting_overlay": { + "$ref": "/schemas/v1/core/targeting.json" + }, + "creative_assignments": { + "type": "array", + "description": "Creative assets assigned to this package", + "items": { + "$ref": "/schemas/v1/core/creative-assignment.json" + } + }, + "status": { + "$ref": "/schemas/v1/enums/package-status.json" + } + }, + "required": ["package_id", "status"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/product.json b/static/schemas/v1/core/product.json new file mode 100644 index 0000000000..1ca542b73a --- /dev/null +++ b/static/schemas/v1/core/product.json @@ -0,0 +1,73 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/product.json", + "title": "Product", + "description": "Represents available advertising inventory", + "type": "object", + "properties": { + "product_id": { + "type": "string", + "description": "Unique identifier for the product" + }, + "name": { + "type": "string", + "description": "Human-readable product name" + }, + "description": { + "type": "string", + "description": "Detailed description of the product and its inventory" + }, + "formats": { + "type": "array", + "description": "Array of supported creative formats", + "items": { + "$ref": "/schemas/v1/core/format.json" + } + }, + "delivery_type": { + "$ref": "/schemas/v1/enums/delivery-type.json" + }, + "is_fixed_price": { + "type": "boolean", + "description": "Whether this product has fixed pricing (true) or uses auction (false)" + }, + "cpm": { + "type": "number", + "description": "Cost per thousand impressions in USD", + "minimum": 0 + }, + "min_spend": { + "type": "number", + "description": "Minimum budget requirement in USD", + "minimum": 0 + }, + "measurement": { + "$ref": "/schemas/v1/core/measurement.json" + }, + "creative_policy": { + "$ref": "/schemas/v1/core/creative-policy.json" + }, + "is_custom": { + "type": "boolean", + "description": "Whether this is a custom product" + }, + "brief_relevance": { + "type": "string", + "description": "Explanation of why this product matches the brief (only included when brief is provided)" + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "Expiration timestamp for custom products" + } + }, + "required": [ + "product_id", + "name", + "description", + "formats", + "delivery_type", + "is_fixed_price" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/response.json b/static/schemas/v1/core/response.json new file mode 100644 index 0000000000..a86a4f153d --- /dev/null +++ b/static/schemas/v1/core/response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/response.json", + "title": "Response", + "description": "Standard response structure (MCP)", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable summary" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "data": { + "description": "Operation-specific data" + }, + "errors": { + "type": "array", + "description": "Non-fatal warnings", + "items": { + "$ref": "/schemas/v1/core/error.json" + } + }, + "clarification_needed": { + "type": "boolean", + "description": "Whether clarification is needed" + } + }, + "required": ["message"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/sub-asset.json b/static/schemas/v1/core/sub-asset.json new file mode 100644 index 0000000000..7325df0c86 --- /dev/null +++ b/static/schemas/v1/core/sub-asset.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/sub-asset.json", + "title": "Sub-Asset", + "description": "Sub-asset for multi-asset creative formats", + "type": "object", + "properties": { + "asset_type": { + "type": "string", + "description": "Type of asset (e.g., product_image, logo, headline)" + }, + "asset_id": { + "type": "string", + "description": "Unique identifier for the asset" + }, + "content_uri": { + "type": "string", + "format": "uri", + "description": "URL for media assets" + }, + "content": { + "type": "array", + "description": "Text content for text assets", + "items": { + "type": "string" + } + } + }, + "required": ["asset_type", "asset_id"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/core/targeting.json b/static/schemas/v1/core/targeting.json new file mode 100644 index 0000000000..12f1b5a751 --- /dev/null +++ b/static/schemas/v1/core/targeting.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/targeting.json", + "title": "Targeting", + "description": "Audience targeting criteria", + "type": "object", + "properties": { + "geo_country_any_of": { + "type": "array", + "description": "Target specific countries (ISO codes)", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + }, + "geo_region_any_of": { + "type": "array", + "description": "Target specific regions/states", + "items": { + "type": "string" + } + }, + "audience_segment_any_of": { + "type": "array", + "description": "Audience segment IDs to target", + "items": { + "type": "string" + } + }, + "axe_include_segment": { + "type": "string", + "description": "AXE segment ID to include for targeting" + }, + "axe_exclude_segment": { + "type": "string", + "description": "AXE segment ID to exclude from targeting" + }, + "signals": { + "type": "array", + "description": "Signal IDs from get_signals", + "items": { + "type": "string" + } + }, + "frequency_cap": { + "$ref": "/schemas/v1/core/frequency-cap.json" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/enums/creative-status.json b/static/schemas/v1/enums/creative-status.json new file mode 100644 index 0000000000..2816f4ae08 --- /dev/null +++ b/static/schemas/v1/enums/creative-status.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/creative-status.json", + "title": "Creative Status", + "description": "Status of a creative asset", + "type": "string", + "enum": ["processing", "approved", "rejected", "pending_review"], + "enumDescriptions": { + "processing": "Creative is being processed or transcoded", + "approved": "Creative has been approved and is ready for delivery", + "rejected": "Creative has been rejected due to policy or technical issues", + "pending_review": "Creative is under review" + } +} \ No newline at end of file diff --git a/static/schemas/v1/enums/delivery-type.json b/static/schemas/v1/enums/delivery-type.json new file mode 100644 index 0000000000..b25ce4a1d4 --- /dev/null +++ b/static/schemas/v1/enums/delivery-type.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/delivery-type.json", + "title": "Delivery Type", + "description": "Type of inventory delivery", + "type": "string", + "enum": ["guaranteed", "non_guaranteed"], + "enumDescriptions": { + "guaranteed": "Reserved inventory with guaranteed delivery", + "non_guaranteed": "Auction-based inventory without delivery guarantees" + } +} \ No newline at end of file diff --git a/static/schemas/v1/enums/frequency-cap-scope.json b/static/schemas/v1/enums/frequency-cap-scope.json new file mode 100644 index 0000000000..e4401bfcb8 --- /dev/null +++ b/static/schemas/v1/enums/frequency-cap-scope.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/frequency-cap-scope.json", + "title": "Frequency Cap Scope", + "description": "Scope for frequency cap application", + "type": "string", + "enum": ["media_buy", "package"], + "enumDescriptions": { + "media_buy": "Apply frequency cap across the entire media buy", + "package": "Apply frequency cap at the package level" + } +} \ No newline at end of file diff --git a/static/schemas/v1/enums/media-buy-status.json b/static/schemas/v1/enums/media-buy-status.json new file mode 100644 index 0000000000..87efb9d926 --- /dev/null +++ b/static/schemas/v1/enums/media-buy-status.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/media-buy-status.json", + "title": "Media Buy Status", + "description": "Status of a media buy", + "type": "string", + "enum": ["pending_activation", "active", "paused", "completed"], + "enumDescriptions": { + "pending_activation": "Media buy created but not yet activated", + "active": "Media buy is currently running", + "paused": "Media buy is temporarily paused", + "completed": "Media buy has finished running" + } +} \ No newline at end of file diff --git a/static/schemas/v1/enums/pacing.json b/static/schemas/v1/enums/pacing.json new file mode 100644 index 0000000000..7a13b5fec7 --- /dev/null +++ b/static/schemas/v1/enums/pacing.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/pacing.json", + "title": "Pacing", + "description": "Budget pacing strategy", + "type": "string", + "enum": ["even", "asap", "front_loaded"], + "enumDescriptions": { + "even": "Spend budget evenly over the campaign duration", + "asap": "Spend budget as quickly as possible", + "front_loaded": "Spend more budget at the beginning of the campaign" + } +} \ No newline at end of file diff --git a/static/schemas/v1/enums/package-status.json b/static/schemas/v1/enums/package-status.json new file mode 100644 index 0000000000..0a32ef8eb6 --- /dev/null +++ b/static/schemas/v1/enums/package-status.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/enums/package-status.json", + "title": "Package Status", + "description": "Status of a package", + "type": "string", + "enum": ["draft", "active", "paused", "completed"], + "enumDescriptions": { + "draft": "Package is in draft state", + "active": "Package is currently active", + "paused": "Package is paused", + "completed": "Package has completed delivery" + } +} \ No newline at end of file diff --git a/static/schemas/v1/index.json b/static/schemas/v1/index.json new file mode 100644 index 0000000000..52a50ae177 --- /dev/null +++ b/static/schemas/v1/index.json @@ -0,0 +1,202 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/index.json", + "title": "AdCP Schema Registry v1", + "version": "1.0.0", + "description": "Registry of all AdCP JSON schemas for validation and discovery", + "lastUpdated": "2025-09-01", + "baseUrl": "/schemas/v1", + "schemas": { + "core": { + "description": "Core data models used throughout AdCP", + "schemas": { + "product": { + "$ref": "/schemas/v1/core/product.json", + "description": "Represents available advertising inventory" + }, + "media-buy": { + "$ref": "/schemas/v1/core/media-buy.json", + "description": "Represents a purchased advertising campaign" + }, + "package": { + "$ref": "/schemas/v1/core/package.json", + "description": "A specific product within a media buy (line item)" + }, + "creative-asset": { + "$ref": "/schemas/v1/core/creative-asset.json", + "description": "Uploaded creative content" + }, + "targeting": { + "$ref": "/schemas/v1/core/targeting.json", + "description": "Audience targeting criteria" + }, + "budget": { + "$ref": "/schemas/v1/core/budget.json", + "description": "Budget configuration for a media buy or package" + }, + "frequency-cap": { + "$ref": "/schemas/v1/core/frequency-cap.json", + "description": "Frequency capping settings" + }, + "format": { + "$ref": "/schemas/v1/core/format.json", + "description": "Represents a creative format with its requirements" + }, + "measurement": { + "$ref": "/schemas/v1/core/measurement.json", + "description": "Measurement capabilities included with a product" + }, + "creative-policy": { + "$ref": "/schemas/v1/core/creative-policy.json", + "description": "Creative requirements and restrictions for a product" + }, + "response": { + "$ref": "/schemas/v1/core/response.json", + "description": "Standard response structure (MCP)" + }, + "error": { + "$ref": "/schemas/v1/core/error.json", + "description": "Standard error structure" + }, + "sub-asset": { + "$ref": "/schemas/v1/core/sub-asset.json", + "description": "Sub-asset for multi-asset creative formats" + }, + "creative-assignment": { + "$ref": "/schemas/v1/core/creative-assignment.json", + "description": "Assignment of a creative asset to a package" + } + } + }, + "enums": { + "description": "Enumerated types and constants", + "schemas": { + "delivery-type": { + "$ref": "/schemas/v1/enums/delivery-type.json", + "description": "Type of inventory delivery" + }, + "media-buy-status": { + "$ref": "/schemas/v1/enums/media-buy-status.json", + "description": "Status of a media buy" + }, + "package-status": { + "$ref": "/schemas/v1/enums/package-status.json", + "description": "Status of a package" + }, + "creative-status": { + "$ref": "/schemas/v1/enums/creative-status.json", + "description": "Status of a creative asset" + }, + "pacing": { + "$ref": "/schemas/v1/enums/pacing.json", + "description": "Budget pacing strategy" + }, + "frequency-cap-scope": { + "$ref": "/schemas/v1/enums/frequency-cap-scope.json", + "description": "Scope for frequency cap application" + } + } + }, + "media-buy": { + "description": "Media buy task request/response schemas", + "tasks": { + "get-products": { + "request": { + "$ref": "/schemas/v1/media-buy/get-products-request.json", + "description": "Request parameters for discovering available advertising products" + }, + "response": { + "$ref": "/schemas/v1/media-buy/get-products-response.json", + "description": "Response payload for get_products task" + } + }, + "list-creative-formats": { + "request": { + "$ref": "/schemas/v1/media-buy/list-creative-formats-request.json", + "description": "Request parameters for discovering supported creative formats" + }, + "response": { + "$ref": "/schemas/v1/media-buy/list-creative-formats-response.json", + "description": "Response payload for list_creative_formats task" + } + }, + "create-media-buy": { + "request": { + "$ref": "/schemas/v1/media-buy/create-media-buy-request.json", + "description": "Request parameters for creating a media buy" + }, + "response": { + "$ref": "/schemas/v1/media-buy/create-media-buy-response.json", + "description": "Response payload for create_media_buy task" + } + }, + "add-creative-assets": { + "request": { + "$ref": "/schemas/v1/media-buy/add-creative-assets-request.json", + "description": "Request parameters for uploading creative assets" + }, + "response": { + "$ref": "/schemas/v1/media-buy/add-creative-assets-response.json", + "description": "Response payload for add_creative_assets task" + } + }, + "update-media-buy": { + "request": { + "$ref": "/schemas/v1/media-buy/update-media-buy-request.json", + "description": "Request parameters for updating campaign and package settings" + }, + "response": { + "$ref": "/schemas/v1/media-buy/update-media-buy-response.json", + "description": "Response payload for update_media_buy task" + } + }, + "get-media-buy-delivery": { + "request": { + "$ref": "/schemas/v1/media-buy/get-media-buy-delivery-request.json", + "description": "Request parameters for retrieving comprehensive delivery metrics" + }, + "response": { + "$ref": "/schemas/v1/media-buy/get-media-buy-delivery-response.json", + "description": "Response payload for get_media_buy_delivery task" + } + } + } + }, + "signals": { + "description": "Signals protocol task request/response schemas", + "tasks": { + "get-signals": { + "request": { + "$ref": "/schemas/v1/signals/get-signals-request.json", + "description": "Request parameters for discovering signals based on description" + }, + "response": { + "$ref": "/schemas/v1/signals/get-signals-response.json", + "description": "Response payload for get_signals task" + } + }, + "activate-signal": { + "request": { + "$ref": "/schemas/v1/signals/activate-signal-request.json", + "description": "Request parameters for activating a signal on a specific platform/account" + }, + "response": { + "$ref": "/schemas/v1/signals/activate-signal-response.json", + "description": "Response payload for activate_signal task" + } + } + } + } + }, + "usage": { + "validation": "Use these schemas to validate AdCP requests and responses", + "codeGeneration": "Generate client SDKs using these schemas", + "documentation": "Reference schemas for API documentation", + "testing": "Validate test fixtures and examples" + }, + "examples": { + "javascriptValidation": "const Ajv = require('ajv'); const ajv = new Ajv(); const schema = require('./schemas/v1/core/product.json'); const validate = ajv.compile(schema);", + "pythonValidation": "import jsonschema; schema = {...}; jsonschema.validate(data, schema)", + "javaValidation": "// Use everit-org/json-schema or similar library" + } +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/add-creative-assets-request.json b/static/schemas/v1/media-buy/add-creative-assets-request.json new file mode 100644 index 0000000000..88afc358d6 --- /dev/null +++ b/static/schemas/v1/media-buy/add-creative-assets-request.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/add-creative-assets-request.json", + "title": "Add Creative Assets Request", + "description": "Request parameters for uploading creative assets", + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Publisher's ID of the media buy to add creatives to" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference for the media buy" + }, + "assets": { + "type": "array", + "description": "Array of creative assets to upload", + "items": { + "$ref": "/schemas/v1/core/creative-asset.json" + } + } + }, + "required": ["assets"], + "oneOf": [ + { + "required": ["media_buy_id"] + }, + { + "required": ["buyer_ref"] + } + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/add-creative-assets-response.json b/static/schemas/v1/media-buy/add-creative-assets-response.json new file mode 100644 index 0000000000..77ba5fea25 --- /dev/null +++ b/static/schemas/v1/media-buy/add-creative-assets-response.json @@ -0,0 +1,88 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/add-creative-assets-response.json", + "title": "Add Creative Assets Response", + "description": "Response payload for add_creative_assets task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "asset_statuses": { + "type": "array", + "description": "Array of status information for each uploaded asset", + "items": { + "type": "object", + "properties": { + "creative_id": { + "type": "string", + "description": "The creative ID from the request" + }, + "status": { + "$ref": "/schemas/v1/enums/creative-status.json" + }, + "platform_id": { + "type": "string", + "description": "Platform-specific ID assigned to the creative" + }, + "review_feedback": { + "type": "string", + "description": "Feedback from platform review (if any)" + }, + "suggested_adaptations": { + "type": "array", + "description": "Array of recommended format adaptations", + "items": { + "type": "object", + "properties": { + "adaptation_id": { + "type": "string", + "description": "Unique identifier for this adaptation" + }, + "format_id": { + "type": "string", + "description": "Target format ID for the adaptation" + }, + "name": { + "type": "string", + "description": "Suggested name for the adapted creative" + }, + "description": { + "type": "string", + "description": "What this adaptation does" + }, + "changes_summary": { + "type": "array", + "description": "List of changes that will be made", + "items": { + "type": "string" + } + }, + "rationale": { + "type": "string", + "description": "Why this adaptation is recommended" + }, + "estimated_performance_lift": { + "type": "number", + "description": "Expected performance improvement (percentage)", + "minimum": 0 + } + }, + "required": ["adaptation_id", "format_id", "name", "description", "changes_summary", "rationale"], + "additionalProperties": false + } + } + }, + "required": ["creative_id", "status"], + "additionalProperties": false + } + } + }, + "required": ["asset_statuses"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/create-media-buy-request.json b/static/schemas/v1/media-buy/create-media-buy-request.json new file mode 100644 index 0000000000..5a497c382f --- /dev/null +++ b/static/schemas/v1/media-buy/create-media-buy-request.json @@ -0,0 +1,64 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/create-media-buy-request.json", + "title": "Create Media Buy Request", + "description": "Request parameters for creating a media buy", + "type": "object", + "properties": { + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this media buy" + }, + "packages": { + "type": "array", + "description": "Array of package configurations", + "items": { + "type": "object", + "properties": { + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this package" + }, + "products": { + "type": "array", + "description": "Array of product IDs to include in this package", + "items": { + "type": "string" + } + }, + "budget": { + "$ref": "/schemas/v1/core/budget.json" + }, + "targeting_overlay": { + "$ref": "/schemas/v1/core/targeting.json" + } + }, + "required": ["buyer_ref", "products"], + "additionalProperties": false + } + }, + "promoted_offering": { + "type": "string", + "description": "Description of advertiser and what is being promoted" + }, + "po_number": { + "type": "string", + "description": "Purchase order number for tracking" + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "Campaign start date/time in ISO 8601 format" + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "Campaign end date/time in ISO 8601 format" + }, + "budget": { + "$ref": "/schemas/v1/core/budget.json" + } + }, + "required": ["buyer_ref", "packages", "promoted_offering", "po_number", "start_time", "end_time", "budget"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/create-media-buy-response.json b/static/schemas/v1/media-buy/create-media-buy-response.json new file mode 100644 index 0000000000..0212b3a74b --- /dev/null +++ b/static/schemas/v1/media-buy/create-media-buy-response.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/create-media-buy-response.json", + "title": "Create Media Buy Response", + "description": "Response payload for create_media_buy task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable confirmation message" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "media_buy_id": { + "type": "string", + "description": "Publisher's unique identifier for the created media buy" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this media buy" + }, + "creative_deadline": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 timestamp for creative upload deadline" + }, + "packages": { + "type": "array", + "description": "Array of created packages", + "items": { + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Publisher's unique identifier for the package" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for the package" + } + }, + "required": ["package_id", "buyer_ref"], + "additionalProperties": false + } + } + }, + "required": ["media_buy_id", "buyer_ref", "packages"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/get-media-buy-delivery-request.json b/static/schemas/v1/media-buy/get-media-buy-delivery-request.json new file mode 100644 index 0000000000..7c3089da0e --- /dev/null +++ b/static/schemas/v1/media-buy/get-media-buy-delivery-request.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/get-media-buy-delivery-request.json", + "title": "Get Media Buy Delivery Request", + "description": "Request parameters for retrieving comprehensive delivery metrics", + "type": "object", + "properties": { + "media_buy_ids": { + "type": "array", + "description": "Array of publisher media buy IDs to get delivery data for", + "items": { + "type": "string" + } + }, + "buyer_refs": { + "type": "array", + "description": "Array of buyer reference IDs to get delivery data for", + "items": { + "type": "string" + } + }, + "status_filter": { + "oneOf": [ + { + "type": "string", + "enum": ["active", "pending", "paused", "completed", "failed", "all"] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": ["active", "pending", "paused", "completed", "failed"] + } + } + ], + "description": "Filter by status. Can be a single status or array of statuses" + }, + "start_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "Start date for reporting period (YYYY-MM-DD)" + }, + "end_date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "End date for reporting period (YYYY-MM-DD)" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/get-media-buy-delivery-response.json b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json new file mode 100644 index 0000000000..0b895e9148 --- /dev/null +++ b/static/schemas/v1/media-buy/get-media-buy-delivery-response.json @@ -0,0 +1,209 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/get-media-buy-delivery-response.json", + "title": "Get Media Buy Delivery Response", + "description": "Response payload for get_media_buy_delivery task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable summary of campaign performance" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "reporting_period": { + "type": "object", + "description": "Date range for the report", + "properties": { + "start": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 start timestamp" + }, + "end": { + "type": "string", + "format": "date-time", + "description": "ISO 8601 end timestamp" + } + }, + "required": ["start", "end"], + "additionalProperties": false + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code", + "pattern": "^[A-Z]{3}$" + }, + "aggregated_totals": { + "type": "object", + "description": "Combined metrics across all returned media buys", + "properties": { + "impressions": { + "type": "number", + "description": "Total impressions delivered across all media buys", + "minimum": 0 + }, + "spend": { + "type": "number", + "description": "Total amount spent across all media buys", + "minimum": 0 + }, + "clicks": { + "type": "number", + "description": "Total clicks across all media buys (if applicable)", + "minimum": 0 + }, + "video_completions": { + "type": "number", + "description": "Total video completions across all media buys (if applicable)", + "minimum": 0 + }, + "media_buy_count": { + "type": "integer", + "description": "Number of media buys included in the response", + "minimum": 0 + } + }, + "required": ["impressions", "spend", "media_buy_count"], + "additionalProperties": false + }, + "deliveries": { + "type": "array", + "description": "Array of delivery data for each media buy", + "items": { + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Publisher's media buy identifier" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this media buy" + }, + "status": { + "type": "string", + "description": "Current media buy status", + "enum": ["pending", "active", "paused", "completed", "failed"] + }, + "totals": { + "type": "object", + "description": "Aggregate metrics for this media buy across all packages", + "properties": { + "impressions": { + "type": "number", + "description": "Total impressions delivered", + "minimum": 0 + }, + "spend": { + "type": "number", + "description": "Total amount spent", + "minimum": 0 + }, + "clicks": { + "type": "number", + "description": "Total clicks (if applicable)", + "minimum": 0 + }, + "ctr": { + "type": "number", + "description": "Click-through rate (clicks/impressions)", + "minimum": 0, + "maximum": 1 + }, + "video_completions": { + "type": "number", + "description": "Total video completions (if applicable)", + "minimum": 0 + }, + "completion_rate": { + "type": "number", + "description": "Video completion rate (completions/impressions)", + "minimum": 0, + "maximum": 1 + } + }, + "required": ["impressions", "spend"], + "additionalProperties": false + }, + "by_package": { + "type": "array", + "description": "Metrics broken down by package", + "items": { + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Publisher's package identifier" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for this package" + }, + "impressions": { + "type": "number", + "description": "Package impressions", + "minimum": 0 + }, + "spend": { + "type": "number", + "description": "Package spend", + "minimum": 0 + }, + "clicks": { + "type": "number", + "description": "Package clicks", + "minimum": 0 + }, + "video_completions": { + "type": "number", + "description": "Package video completions", + "minimum": 0 + }, + "pacing_index": { + "type": "number", + "description": "Delivery pace (1.0 = on track, <1.0 = behind, >1.0 = ahead)", + "minimum": 0 + } + }, + "required": ["package_id", "impressions", "spend"], + "additionalProperties": false + } + }, + "daily_breakdown": { + "type": "array", + "description": "Day-by-day delivery", + "items": { + "type": "object", + "properties": { + "date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$", + "description": "Date (YYYY-MM-DD)" + }, + "impressions": { + "type": "number", + "description": "Daily impressions", + "minimum": 0 + }, + "spend": { + "type": "number", + "description": "Daily spend", + "minimum": 0 + } + }, + "required": ["date", "impressions", "spend"], + "additionalProperties": false + } + } + }, + "required": ["media_buy_id", "status", "totals", "by_package"], + "additionalProperties": false + } + } + }, + "required": ["reporting_period", "currency", "aggregated_totals", "deliveries"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/get-products-request.json b/static/schemas/v1/media-buy/get-products-request.json new file mode 100644 index 0000000000..3e70f671b5 --- /dev/null +++ b/static/schemas/v1/media-buy/get-products-request.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/get-products-request.json", + "title": "Get Products Request", + "description": "Request parameters for discovering available advertising products", + "type": "object", + "properties": { + "brief": { + "type": "string", + "description": "Natural language description of campaign requirements" + }, + "promoted_offering": { + "type": "string", + "description": "Description of advertiser and what is being promoted" + }, + "filters": { + "type": "object", + "description": "Structured filters for product discovery", + "properties": { + "delivery_type": { + "$ref": "/schemas/v1/enums/delivery-type.json" + }, + "formats": { + "type": "array", + "description": "Filter by specific formats", + "items": { + "type": "string" + } + }, + "is_fixed_price": { + "type": "boolean", + "description": "Filter for fixed price vs auction products" + }, + "format_types": { + "type": "array", + "description": "Filter by format types", + "items": { + "type": "string", + "enum": ["video", "display", "audio"] + } + }, + "format_ids": { + "type": "array", + "description": "Filter by specific format IDs", + "items": { + "type": "string" + } + }, + "standard_formats_only": { + "type": "boolean", + "description": "Only return products accepting IAB standard formats" + } + }, + "additionalProperties": false + } + }, + "required": ["promoted_offering"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/get-products-response.json b/static/schemas/v1/media-buy/get-products-response.json new file mode 100644 index 0000000000..1bc2fdecda --- /dev/null +++ b/static/schemas/v1/media-buy/get-products-response.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/get-products-response.json", + "title": "Get Products Response", + "description": "Response payload for get_products task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable summary of the response" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "products": { + "type": "array", + "description": "Array of matching products", + "items": { + "$ref": "/schemas/v1/core/product.json" + } + }, + "clarification_needed": { + "type": "boolean", + "description": "Whether clarification is needed" + }, + "errors": { + "type": "array", + "description": "Non-fatal warnings", + "items": { + "$ref": "/schemas/v1/core/error.json" + } + } + }, + "required": ["products"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/list-creative-formats-request.json b/static/schemas/v1/media-buy/list-creative-formats-request.json new file mode 100644 index 0000000000..0444d1379d --- /dev/null +++ b/static/schemas/v1/media-buy/list-creative-formats-request.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/list-creative-formats-request.json", + "title": "List Creative Formats Request", + "description": "Request parameters for discovering supported creative formats", + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Filter by format type", + "enum": ["audio", "video", "display"] + }, + "standard_only": { + "type": "boolean", + "description": "Only return IAB standard formats" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/list-creative-formats-response.json b/static/schemas/v1/media-buy/list-creative-formats-response.json new file mode 100644 index 0000000000..d67ca7c572 --- /dev/null +++ b/static/schemas/v1/media-buy/list-creative-formats-response.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/list-creative-formats-response.json", + "title": "List Creative Formats Response", + "description": "Response payload for list_creative_formats task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable summary of available formats" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "formats": { + "type": "array", + "description": "Array of available creative formats", + "items": { + "$ref": "/schemas/v1/core/format.json" + } + } + }, + "required": ["formats"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/update-media-buy-request.json b/static/schemas/v1/media-buy/update-media-buy-request.json new file mode 100644 index 0000000000..c9aae50ef4 --- /dev/null +++ b/static/schemas/v1/media-buy/update-media-buy-request.json @@ -0,0 +1,86 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/update-media-buy-request.json", + "title": "Update Media Buy Request", + "description": "Request parameters for updating campaign and package settings", + "type": "object", + "properties": { + "media_buy_id": { + "type": "string", + "description": "Publisher's ID of the media buy to update" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference for the media buy to update" + }, + "active": { + "type": "boolean", + "description": "Pause/resume the entire media buy" + }, + "start_time": { + "type": "string", + "format": "date-time", + "description": "New start date/time in ISO 8601 format" + }, + "end_time": { + "type": "string", + "format": "date-time", + "description": "New end date/time in ISO 8601 format" + }, + "budget": { + "$ref": "/schemas/v1/core/budget.json" + }, + "packages": { + "type": "array", + "description": "Package-specific updates", + "items": { + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Publisher's ID of package to update" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference for the package to update" + }, + "budget": { + "$ref": "/schemas/v1/core/budget.json" + }, + "active": { + "type": "boolean", + "description": "Pause/resume specific package" + }, + "targeting_overlay": { + "$ref": "/schemas/v1/core/targeting.json" + }, + "creative_ids": { + "type": "array", + "description": "Update creative assignments", + "items": { + "type": "string" + } + } + }, + "oneOf": [ + { + "required": ["package_id"] + }, + { + "required": ["buyer_ref"] + } + ], + "additionalProperties": false + } + } + }, + "oneOf": [ + { + "required": ["media_buy_id"] + }, + { + "required": ["buyer_ref"] + } + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/media-buy/update-media-buy-response.json b/static/schemas/v1/media-buy/update-media-buy-response.json new file mode 100644 index 0000000000..9ddb6c924d --- /dev/null +++ b/static/schemas/v1/media-buy/update-media-buy-response.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/media-buy/update-media-buy-response.json", + "title": "Update Media Buy Response", + "description": "Response payload for update_media_buy task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable confirmation of changes made" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "media_buy_id": { + "type": "string", + "description": "Publisher's identifier for the media buy" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference identifier for the media buy" + }, + "implementation_date": { + "type": ["string", "null"], + "format": "date-time", + "description": "ISO 8601 timestamp when changes take effect (null if pending approval)" + }, + "affected_packages": { + "type": "array", + "description": "Array of packages that were modified", + "items": { + "type": "object", + "properties": { + "package_id": { + "type": "string", + "description": "Publisher's package identifier" + }, + "buyer_ref": { + "type": "string", + "description": "Buyer's reference for the package" + } + }, + "required": ["package_id", "buyer_ref"], + "additionalProperties": false + } + } + }, + "required": ["media_buy_id", "buyer_ref", "affected_packages"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/signals/activate-signal-request.json b/static/schemas/v1/signals/activate-signal-request.json new file mode 100644 index 0000000000..b37a55a84c --- /dev/null +++ b/static/schemas/v1/signals/activate-signal-request.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/signals/activate-signal-request.json", + "title": "Activate Signal Request", + "description": "Request parameters for activating a signal on a specific platform/account", + "type": "object", + "properties": { + "signal_agent_segment_id": { + "type": "string", + "description": "The universal identifier for the signal to activate" + }, + "platform": { + "type": "string", + "description": "The target platform for activation" + }, + "account": { + "type": "string", + "description": "Account identifier (required for account-specific activation)" + } + }, + "required": ["signal_agent_segment_id", "platform"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/signals/activate-signal-response.json b/static/schemas/v1/signals/activate-signal-response.json new file mode 100644 index 0000000000..247bd614a1 --- /dev/null +++ b/static/schemas/v1/signals/activate-signal-response.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/signals/activate-signal-response.json", + "title": "Activate Signal Response", + "description": "Response payload for activate_signal task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable status message" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "task_id": { + "type": "string", + "description": "Unique identifier for tracking the activation" + }, + "status": { + "type": "string", + "description": "Current status", + "enum": ["pending", "processing", "deployed", "failed"] + }, + "decisioning_platform_segment_id": { + "type": "string", + "description": "The platform-specific ID to use once activated" + }, + "estimated_activation_duration_minutes": { + "type": "number", + "description": "Estimated time to complete (optional)", + "minimum": 0 + }, + "deployed_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when activation completed (optional)" + }, + "error": { + "type": "object", + "description": "Error details if activation failed (optional)", + "properties": { + "code": { + "type": "string", + "description": "Error code for programmatic handling" + }, + "message": { + "type": "string", + "description": "Detailed error message" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["task_id", "status"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/signals/get-signals-request.json b/static/schemas/v1/signals/get-signals-request.json new file mode 100644 index 0000000000..398197212b --- /dev/null +++ b/static/schemas/v1/signals/get-signals-request.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/signals/get-signals-request.json", + "title": "Get Signals Request", + "description": "Request parameters for discovering signals based on description", + "type": "object", + "properties": { + "signal_spec": { + "type": "string", + "description": "Natural language description of the desired signals" + }, + "deliver_to": { + "type": "object", + "description": "Where the signals need to be delivered", + "properties": { + "platforms": { + "oneOf": [ + { + "type": "string", + "const": "all" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "description": "Target platforms for signal deployment" + }, + "accounts": { + "type": "array", + "description": "Specific platform-account combinations", + "items": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "description": "Platform identifier" + }, + "account": { + "type": "string", + "description": "Account identifier on that platform" + } + }, + "required": ["platform", "account"], + "additionalProperties": false + } + }, + "countries": { + "type": "array", + "description": "Countries where signals will be used (ISO codes)", + "items": { + "type": "string", + "pattern": "^[A-Z]{2}$" + } + } + }, + "required": ["platforms", "countries"], + "additionalProperties": false + }, + "filters": { + "type": "object", + "description": "Filters to refine results", + "properties": { + "catalog_types": { + "type": "array", + "description": "Filter by catalog type", + "items": { + "type": "string", + "enum": ["marketplace", "custom", "owned"] + } + }, + "data_providers": { + "type": "array", + "description": "Filter by specific data providers", + "items": { + "type": "string" + } + }, + "max_cpm": { + "type": "number", + "description": "Maximum CPM price filter", + "minimum": 0 + }, + "min_coverage_percentage": { + "type": "number", + "description": "Minimum coverage requirement", + "minimum": 0, + "maximum": 100 + } + }, + "additionalProperties": false + }, + "max_results": { + "type": "integer", + "description": "Maximum number of results to return", + "minimum": 1 + } + }, + "required": ["signal_spec", "deliver_to"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/v1/signals/get-signals-response.json b/static/schemas/v1/signals/get-signals-response.json new file mode 100644 index 0000000000..a8863be15c --- /dev/null +++ b/static/schemas/v1/signals/get-signals-response.json @@ -0,0 +1,112 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/signals/get-signals-response.json", + "title": "Get Signals Response", + "description": "Response payload for get_signals task", + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Human-readable summary of signals found" + }, + "context_id": { + "type": "string", + "description": "Session continuity identifier" + }, + "signals": { + "type": "array", + "description": "Array of matching signals", + "items": { + "type": "object", + "properties": { + "signal_agent_segment_id": { + "type": "string", + "description": "Unique identifier for the signal" + }, + "name": { + "type": "string", + "description": "Human-readable signal name" + }, + "description": { + "type": "string", + "description": "Detailed signal description" + }, + "signal_type": { + "type": "string", + "description": "Type of signal", + "enum": ["marketplace", "custom", "owned"] + }, + "data_provider": { + "type": "string", + "description": "Name of the data provider" + }, + "coverage_percentage": { + "type": "number", + "description": "Percentage of audience coverage", + "minimum": 0, + "maximum": 100 + }, + "deployments": { + "type": "array", + "description": "Array of platform deployments", + "items": { + "type": "object", + "properties": { + "platform": { + "type": "string", + "description": "Platform name" + }, + "account": { + "type": ["string", "null"], + "description": "Specific account if applicable" + }, + "is_live": { + "type": "boolean", + "description": "Whether signal is currently active" + }, + "scope": { + "type": "string", + "description": "Deployment scope", + "enum": ["platform-wide", "account-specific"] + }, + "decisioning_platform_segment_id": { + "type": "string", + "description": "Platform-specific segment ID" + }, + "estimated_activation_duration_minutes": { + "type": "number", + "description": "Time to activate if not live", + "minimum": 0 + } + }, + "required": ["platform", "is_live", "scope"], + "additionalProperties": false + } + }, + "pricing": { + "type": "object", + "description": "Pricing information", + "properties": { + "cpm": { + "type": "number", + "description": "Cost per thousand impressions", + "minimum": 0 + }, + "currency": { + "type": "string", + "description": "Currency code", + "pattern": "^[A-Z]{3}$" + } + }, + "required": ["cpm", "currency"], + "additionalProperties": false + } + }, + "required": ["signal_agent_segment_id", "name", "description", "signal_type", "data_provider", "coverage_percentage", "deployments", "pricing"], + "additionalProperties": false + } + } + }, + "required": ["signals"], + "additionalProperties": false +} \ No newline at end of file diff --git a/static/schemas/validation-example.js b/static/schemas/validation-example.js new file mode 100644 index 0000000000..36bc0a8a81 --- /dev/null +++ b/static/schemas/validation-example.js @@ -0,0 +1,57 @@ +/** + * Simple validation example using the AdCP JSON schemas + * Run with: node validation-example.js + */ + +const fs = require('fs'); +const path = require('path'); + +// Simple JSON schema validator (minimal implementation) +function validateSchema(data, schema) { + // This is a very basic validator - in production use ajv or similar + if (schema.type === 'object') { + if (typeof data !== 'object' || data === null) { + return { valid: false, errors: [`Expected object, got ${typeof data}`] }; + } + + // Check required fields + const errors = []; + if (schema.required) { + for (const field of schema.required) { + if (!(field in data)) { + errors.push(`Missing required field: ${field}`); + } + } + } + + return { valid: errors.length === 0, errors }; + } + + return { valid: true, errors: [] }; +} + +// Load and validate a Product example +const productSchema = JSON.parse(fs.readFileSync(path.join(__dirname, 'v1/core/product.json'), 'utf8')); + +const exampleProduct = { + "product_id": "ctv_sports_premium", + "name": "CTV Sports Premium", + "description": "Premium CTV inventory on sports content", + "formats": [{"format_id": "video_16x9_30s", "name": "30-second video"}], + "delivery_type": "guaranteed", + "is_fixed_price": true +}; + +const result = validateSchema(exampleProduct, productSchema); + +console.log('AdCP Schema Validation Example'); +console.log('=============================='); +console.log('Schema:', productSchema.title); +console.log('Valid:', result.valid); +if (!result.valid) { + console.log('Errors:', result.errors); +} +console.log(''); +console.log('Example Product:', JSON.stringify(exampleProduct, null, 2)); +console.log(''); +console.log('✅ Schema validation working! Use a proper library like ajv for production.'); \ No newline at end of file diff --git a/tests/example-validation-simple.test.js b/tests/example-validation-simple.test.js new file mode 100644 index 0000000000..24f303742e --- /dev/null +++ b/tests/example-validation-simple.test.js @@ -0,0 +1,163 @@ +#!/usr/bin/env node +/** + * Simple example data validation tests + * Validates that basic example data from documentation matches the schemas + */ + +const fs = require('fs'); +const path = require('path'); +const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); + +const SCHEMA_BASE_DIR = path.join(__dirname, '../static/schemas/v1'); + +// Schema loader for resolving $ref +async function loadExternalSchema(uri) { + if (uri.startsWith('/schemas/v1/')) { + const schemaPath = path.join(SCHEMA_BASE_DIR, uri.replace('/schemas/v1/', '')); + try { + const content = fs.readFileSync(schemaPath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load referenced schema ${uri}: ${error.message}`); + } + } + throw new Error(`Cannot load external schema: ${uri}`); +} + +let totalTests = 0; +let passedTests = 0; +let failedTests = 0; + +function log(message, type = 'info') { + const colors = { + info: '\x1b[0m', + success: '\x1b[32m', + error: '\x1b[31m', + warning: '\x1b[33m' + }; + console.log(`${colors[type]}${message}\x1b[0m`); +} + +async function validateExample(data, schemaId, description) { + totalTests++; + try { + // Create fresh AJV instance for each validation + const ajv = new Ajv({ + allErrors: true, + verbose: false, + strict: false, + loadSchema: loadExternalSchema + }); + addFormats(ajv); + + // Load the specific schema + const schemaPath = path.join(SCHEMA_BASE_DIR, schemaId.replace('/schemas/v1/', '')); + const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); + + // Compile and validate + const validate = await ajv.compileAsync(schema); + const isValid = validate(data); + + if (isValid) { + log(`✅ ${description}`, 'success'); + passedTests++; + } else { + const errors = validate.errors.map(err => + `${err.instancePath || 'root'}: ${err.message}` + ).join('; '); + log(`❌ ${description}: ${errors}`, 'error'); + failedTests++; + } + } catch (error) { + log(`❌ ${description}: ${error.message}`, 'error'); + failedTests++; + } +} + +async function runTests() { + log('🧪 Starting Example Data Validation Tests', 'info'); + log('==========================================='); + + // Simple examples that don't depend on complex references + const simpleExamples = [ + { + data: { "code": "INVALID_REQUEST", "message": "Missing required field" }, + schema: '/schemas/v1/core/error.json', + description: 'Error example' + }, + { + data: { "message": "Operation completed successfully" }, + schema: '/schemas/v1/core/response.json', + description: 'Response example' + }, + { + data: { "format_id": "video_standard_30s", "name": "Standard Video - 30 seconds" }, + schema: '/schemas/v1/core/format.json', + description: 'Format example' + }, + { + data: { + "type": "incremental_sales_lift", + "attribution": "deterministic_purchase", + "reporting": "weekly_dashboard" + }, + schema: '/schemas/v1/core/measurement.json', + description: 'Measurement example' + }, + { + data: { + "co_branding": "optional", + "landing_page": "any", + "templates_available": true + }, + schema: '/schemas/v1/core/creative-policy.json', + description: 'Creative Policy example' + } + ]; + + // Test simple examples + for (const example of simpleExamples) { + await validateExample(example.data, example.schema, example.description); + } + + // Test request/response examples + await validateExample( + { + "promoted_offering": "Nike Air Max 2024", + "brief": "Premium video inventory" + }, + '/schemas/v1/media-buy/get-products-request.json', + 'get_products request' + ); + + await validateExample( + { + "signal_spec": "High-income households", + "deliver_to": { + "platforms": ["the-trade-desk"], + "countries": ["US"] + } + }, + '/schemas/v1/signals/get-signals-request.json', + 'get_signals request' + ); + + // Print results + log('\n==========================================='); + log(`Tests completed: ${totalTests}`); + log(`✅ Passed: ${passedTests}`, 'success'); + log(`❌ Failed: ${failedTests}`, failedTests > 0 ? 'error' : 'success'); + + if (failedTests > 0) { + process.exit(1); + } else { + log('\n🎉 All example validation tests passed!', 'success'); + } +} + +// Run the tests +runTests().catch(error => { + log(`Test execution failed: ${error.message}`, 'error'); + process.exit(1); +}); \ No newline at end of file diff --git a/tests/example-validation.test.js b/tests/example-validation.test.js new file mode 100644 index 0000000000..605a49d4cf --- /dev/null +++ b/tests/example-validation.test.js @@ -0,0 +1,477 @@ +#!/usr/bin/env node +/** + * Example data validation tests + * Validates that example data from documentation matches the schemas + */ + +const fs = require('fs'); +const path = require('path'); +const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); + +const SCHEMA_BASE_DIR = path.join(__dirname, '../static/schemas/v1'); + +// Initialize AJV with formats +const ajv = new Ajv({ + allErrors: true, + verbose: true, + strict: false +}); +addFormats(ajv); + +// Schema loader for resolving $ref +async function loadExternalSchema(uri) { + if (uri.startsWith('/schemas/v1/')) { + const schemaPath = path.join(SCHEMA_BASE_DIR, uri.replace('/schemas/v1/', '')); + try { + const content = fs.readFileSync(schemaPath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load referenced schema ${uri}: ${error.message}`); + } + } + throw new Error(`Cannot load external schema: ${uri}`); +} + +// Load all schemas with async compilation +const schemas = {}; +async function loadSchemas(dir) { + const items = fs.readdirSync(dir); + + for (const item of items) { + const itemPath = path.join(dir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + await loadSchemas(itemPath); + } else if (item.endsWith('.json') && item !== 'index.json') { + try { + const schema = JSON.parse(fs.readFileSync(itemPath, 'utf8')); + if (schema.$id) { + // Create a fresh AJV instance for each schema to avoid conflicts + const schemaAjv = new Ajv({ + allErrors: true, + verbose: true, + strict: false, + loadSchema: loadExternalSchema + }); + addFormats(schemaAjv); + + schemas[schema.$id] = await schemaAjv.compileAsync(schema); + } + } catch (error) { + console.error(`Failed to load schema ${itemPath}: ${error.message}`); + } + } + } +} + +let totalTests = 0; +let passedTests = 0; +let failedTests = 0; + +function log(message, type = 'info') { + const colors = { + info: '\x1b[0m', + success: '\x1b[32m', + error: '\x1b[31m', + warning: '\x1b[33m' + }; + console.log(`${colors[type]}${message}\x1b[0m`); +} + +function test(description, testFn) { + totalTests++; + try { + const result = testFn(); + if (result === true || result === undefined) { + log(`✅ ${description}`, 'success'); + passedTests++; + } else { + log(`❌ ${description}: ${result}`, 'error'); + failedTests++; + } + } catch (error) { + log(`❌ ${description}: ${error.message}`, 'error'); + failedTests++; + } +} + +function validateAgainstSchema(data, schemaId, dataDescription) { + const validator = schemas[schemaId]; + if (!validator) { + throw new Error(`Schema not found: ${schemaId}`); + } + + const isValid = validator(data); + if (!isValid) { + const errors = validator.errors.map(err => + `${err.instancePath || 'root'}: ${err.message}` + ).join('; '); + return `${dataDescription} - Validation errors: ${errors}`; + } + + return true; +} + +// Example data from documentation +const exampleData = { + // Core data models + product: { + "product_id": "ctv_sports_premium", + "name": "CTV Sports Premium", + "description": "Premium CTV inventory on sports content", + "formats": [{"format_id": "video_16x9_30s", "name": "30-second video"}], + "delivery_type": "guaranteed", + "is_fixed_price": true, + "cpm": 45.00, + "min_spend": 10000 + }, + + mediaBuy: { + "media_buy_id": "mb_12345", + "status": "active", + "promoted_offering": "Nike Air Max 2024 - latest innovation in cushioning", + "total_budget": 50000, + "packages": [] + }, + + package: { + "package_id": "pkg_ctv_001", + "status": "active" + }, + + creativeAsset: { + "creative_id": "hero_video_30s", + "name": "Nike Air Max Hero 30s", + "format": "video" + }, + + targeting: { + "geo_country_any_of": ["US"], + "audience_segment_any_of": ["sports_enthusiasts"] + }, + + budget: { + "total": 50000, + "currency": "USD", + "pacing": "even" + }, + + frequencyCap: { + "suppress_minutes": 1440, + "scope": "media_buy" + }, + + format: { + "format_id": "video_standard_30s", + "name": "Standard Video - 30 seconds" + }, + + measurement: { + "type": "incremental_sales_lift", + "attribution": "deterministic_purchase", + "reporting": "weekly_dashboard" + }, + + creativePolicy: { + "co_branding": "optional", + "landing_page": "any", + "templates_available": true + }, + + error: { + "code": "INVALID_REQUEST", + "message": "Missing required field" + }, + + response: { + "message": "Operation completed successfully" + }, + + // Request/Response examples + getProductsRequest: { + "promoted_offering": "Nike Air Max 2024 - latest innovation in cushioning", + "brief": "Premium video inventory for sports fans" + }, + + getProductsResponse: { + "products": [ + { + "product_id": "ctv_sports_premium", + "name": "CTV Sports Premium", + "description": "Premium CTV inventory on sports content", + "formats": [{"format_id": "video_16x9_30s", "name": "30-second video"}], + "delivery_type": "guaranteed", + "is_fixed_price": true, + "cpm": 45.00, + "min_spend": 10000 + } + ] + }, + + createMediaBuyRequest: { + "buyer_ref": "nike_q1_campaign_2024", + "packages": [ + { + "buyer_ref": "nike_ctv_sports_package", + "products": ["ctv_sports_premium"] + } + ], + "promoted_offering": "Nike Air Max 2024 - latest innovation in cushioning", + "po_number": "PO-2024-001", + "start_time": "2024-01-01T00:00:00Z", + "end_time": "2024-01-31T23:59:59Z", + "budget": { + "total": 50000, + "currency": "USD" + } + }, + + createMediaBuyResponse: { + "media_buy_id": "mb_12345", + "buyer_ref": "nike_q1_campaign_2024", + "packages": [ + { + "package_id": "pkg_12345_001", + "buyer_ref": "nike_ctv_sports_package" + } + ] + }, + + // Signals examples + getSignalsRequest: { + "signal_spec": "High-income households interested in luxury goods", + "deliver_to": { + "platforms": ["the-trade-desk", "amazon-dsp"], + "countries": ["US"] + } + }, + + getSignalsResponse: { + "signals": [ + { + "signal_agent_segment_id": "luxury_auto_intenders", + "name": "Luxury Automotive Intenders", + "description": "High-income individuals researching luxury vehicles", + "signal_type": "marketplace", + "data_provider": "Experian", + "coverage_percentage": 12, + "deployments": [ + { + "platform": "the-trade-desk", + "account": null, + "is_live": true, + "scope": "platform-wide", + "decisioning_platform_segment_id": "ttd_exp_lux_auto_123" + } + ], + "pricing": { + "cpm": 3.50, + "currency": "USD" + } + } + ] + }, + + activateSignalRequest: { + "signal_agent_segment_id": "luxury_auto_intenders", + "platform": "the-trade-desk", + "account": "agency-123-ttd" + }, + + activateSignalResponse: { + "task_id": "activation_789", + "status": "pending", + "decisioning_platform_segment_id": "ttd_agency123_lux_auto" + } +}; + +// Main test execution +async function runTests() { + log('🧪 Starting Example Data Validation Tests', 'info'); + log('==========================================='); + + // Load all schemas first + await loadSchemas(SCHEMA_BASE_DIR); + + // Core data model tests + test('Product example validates against schema', () => { + return validateAgainstSchema( + exampleData.product, + '/schemas/v1/core/product.json', + 'Product example' + ); +}); + +test('Media Buy example validates against schema', () => { + return validateAgainstSchema( + exampleData.mediaBuy, + '/schemas/v1/core/media-buy.json', + 'Media Buy example' + ); +}); + +test('Package example validates against schema', () => { + return validateAgainstSchema( + exampleData.package, + '/schemas/v1/core/package.json', + 'Package example' + ); +}); + +test('Creative Asset example validates against schema', () => { + return validateAgainstSchema( + exampleData.creativeAsset, + '/schemas/v1/core/creative-asset.json', + 'Creative Asset example' + ); +}); + +test('Targeting example validates against schema', () => { + return validateAgainstSchema( + exampleData.targeting, + '/schemas/v1/core/targeting.json', + 'Targeting example' + ); +}); + +test('Budget example validates against schema', () => { + return validateAgainstSchema( + exampleData.budget, + '/schemas/v1/core/budget.json', + 'Budget example' + ); +}); + +test('Frequency Cap example validates against schema', () => { + return validateAgainstSchema( + exampleData.frequencyCap, + '/schemas/v1/core/frequency-cap.json', + 'Frequency Cap example' + ); +}); + +test('Format example validates against schema', () => { + return validateAgainstSchema( + exampleData.format, + '/schemas/v1/core/format.json', + 'Format example' + ); +}); + +test('Measurement example validates against schema', () => { + return validateAgainstSchema( + exampleData.measurement, + '/schemas/v1/core/measurement.json', + 'Measurement example' + ); +}); + +test('Creative Policy example validates against schema', () => { + return validateAgainstSchema( + exampleData.creativePolicy, + '/schemas/v1/core/creative-policy.json', + 'Creative Policy example' + ); +}); + +test('Error example validates against schema', () => { + return validateAgainstSchema( + exampleData.error, + '/schemas/v1/core/error.json', + 'Error example' + ); +}); + +test('Response example validates against schema', () => { + return validateAgainstSchema( + exampleData.response, + '/schemas/v1/core/response.json', + 'Response example' + ); +}); + +// Request/Response tests +test('get_products request validates against schema', () => { + return validateAgainstSchema( + exampleData.getProductsRequest, + '/schemas/v1/media-buy/get-products-request.json', + 'get_products request' + ); +}); + +test('get_products response validates against schema', () => { + return validateAgainstSchema( + exampleData.getProductsResponse, + '/schemas/v1/media-buy/get-products-response.json', + 'get_products response' + ); +}); + +test('create_media_buy request validates against schema', () => { + return validateAgainstSchema( + exampleData.createMediaBuyRequest, + '/schemas/v1/media-buy/create-media-buy-request.json', + 'create_media_buy request' + ); +}); + +test('create_media_buy response validates against schema', () => { + return validateAgainstSchema( + exampleData.createMediaBuyResponse, + '/schemas/v1/media-buy/create-media-buy-response.json', + 'create_media_buy response' + ); +}); + +test('get_signals request validates against schema', () => { + return validateAgainstSchema( + exampleData.getSignalsRequest, + '/schemas/v1/signals/get-signals-request.json', + 'get_signals request' + ); +}); + +test('get_signals response validates against schema', () => { + return validateAgainstSchema( + exampleData.getSignalsResponse, + '/schemas/v1/signals/get-signals-response.json', + 'get_signals response' + ); +}); + +test('activate_signal request validates against schema', () => { + return validateAgainstSchema( + exampleData.activateSignalRequest, + '/schemas/v1/signals/activate-signal-request.json', + 'activate_signal request' + ); +}); + +test('activate_signal response validates against schema', () => { + return validateAgainstSchema( + exampleData.activateSignalResponse, + '/schemas/v1/signals/activate-signal-response.json', + 'activate_signal response' + ); +}); + + // Print results + log('\n==========================================='); + log(`Tests completed: ${totalTests}`); + log(`✅ Passed: ${passedTests}`, 'success'); + log(`❌ Failed: ${failedTests}`, failedTests > 0 ? 'error' : 'success'); + + if (failedTests > 0) { + process.exit(1); + } else { + log('\n🎉 All example validation tests passed!', 'success'); + } +} + +// Run the tests +runTests().catch(error => { + log(`Test execution failed: ${error.message}`, 'error'); + process.exit(1); +}); \ No newline at end of file diff --git a/tests/schema-validation.test.js b/tests/schema-validation.test.js new file mode 100644 index 0000000000..914ad61a01 --- /dev/null +++ b/tests/schema-validation.test.js @@ -0,0 +1,328 @@ +#!/usr/bin/env node +/** + * JSON Schema validation test suite + * Validates that all schemas are syntactically correct and cross-references resolve + */ + +const fs = require('fs'); +const path = require('path'); +const Ajv = require('ajv'); +const addFormats = require('ajv-formats'); + +const SCHEMA_BASE_DIR = path.join(__dirname, '../static/schemas/v1'); + +// Initialize AJV with formats and custom loader +const ajv = new Ajv({ + allErrors: true, + verbose: true, + strict: false, // Allow some flexibility for our schema structure + loadSchema: loadExternalSchema +}); +addFormats(ajv); + +// Schema loader for resolving $ref +async function loadExternalSchema(uri) { + if (uri.startsWith('/schemas/v1/')) { + const schemaPath = path.join(SCHEMA_BASE_DIR, uri.replace('/schemas/v1/', '')); + try { + const content = fs.readFileSync(schemaPath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load referenced schema ${uri}: ${error.message}`); + } + } + throw new Error(`Cannot load external schema: ${uri}`); +} + +let totalTests = 0; +let passedTests = 0; +let failedTests = 0; + +function log(message, type = 'info') { + const colors = { + info: '\x1b[0m', + success: '\x1b[32m', + error: '\x1b[31m', + warning: '\x1b[33m' + }; + console.log(`${colors[type]}${message}\x1b[0m`); +} + +async function test(description, testFn) { + totalTests++; + try { + const result = await testFn(); + if (result === true || result === undefined) { + log(`✅ ${description}`, 'success'); + passedTests++; + } else { + log(`❌ ${description}: ${result}`, 'error'); + failedTests++; + } + } catch (error) { + log(`❌ ${description}: ${error.message}`, 'error'); + failedTests++; + } +} + +function loadSchema(schemaPath) { + try { + const content = fs.readFileSync(schemaPath, 'utf8'); + return JSON.parse(content); + } catch (error) { + throw new Error(`Failed to load schema ${schemaPath}: ${error.message}`); + } +} + +function findAllSchemas(dir) { + const schemas = []; + + function traverse(currentDir) { + const items = fs.readdirSync(currentDir); + + for (const item of items) { + const itemPath = path.join(currentDir, item); + const stat = fs.statSync(itemPath); + + if (stat.isDirectory()) { + traverse(itemPath); + } else if (item.endsWith('.json') && item !== 'index.json') { + schemas.push(itemPath); + } + } + } + + traverse(dir); + return schemas; +} + +function validateSchemaStructure(schemaPath, schema) { + // Check required top-level fields + if (!schema.$schema) { + return 'Missing $schema field'; + } + + if (!schema.$id) { + return 'Missing $id field'; + } + + if (!schema.title) { + return 'Missing title field'; + } + + if (!schema.description) { + return 'Missing description field'; + } + + // Validate $schema format + if (!schema.$schema.startsWith('http://json-schema.org/')) { + return 'Invalid $schema URL format'; + } + + // Validate $id format (should be relative path) + if (!schema.$id.startsWith('/schemas/v1/')) { + return `Invalid $id format: ${schema.$id} (should start with /schemas/v1/)`; + } + + return true; +} + +function validateCrossReferences(schemas) { + const schemaIds = new Set(schemas.map(([_, schema]) => schema.$id)); + const missingRefs = []; + + for (const [schemaPath, schema] of schemas) { + // Find all $ref occurrences + const refs = JSON.stringify(schema).match(/"\$ref":\s*"([^"]+)"/g) || []; + + for (const refMatch of refs) { + const ref = refMatch.match(/"\$ref":\s*"([^"]+)"/)[1]; + + // Skip external references (http://, https://) + if (ref.startsWith('http://') || ref.startsWith('https://')) { + continue; + } + + // Check if referenced schema exists + if (!schemaIds.has(ref)) { + missingRefs.push({ schema: schemaPath, ref }); + } + } + } + + if (missingRefs.length > 0) { + const errorMsg = missingRefs.map(({ schema, ref }) => + `${path.basename(schema)} -> ${ref}` + ).join(', '); + return `Missing referenced schemas: ${errorMsg}`; + } + + return true; +} + +function validateRegistryConsistency() { + const registryPath = path.join(SCHEMA_BASE_DIR, 'index.json'); + const registry = loadSchema(registryPath); + + // Collect all schema references from the registry + const registryRefs = new Set(); + + function collectRefs(obj) { + if (typeof obj === 'object' && obj !== null) { + if (obj.$ref) { + registryRefs.add(obj.$ref); + } + for (const value of Object.values(obj)) { + collectRefs(value); + } + } + } + + collectRefs(registry); + + // Find all actual schemas + const actualSchemas = findAllSchemas(SCHEMA_BASE_DIR); + const actualSchemaIds = actualSchemas + .map(schemaPath => loadSchema(schemaPath).$id); + + // Check that all registry references exist + const missingSchemas = []; + for (const ref of registryRefs) { + if (!actualSchemaIds.includes(ref)) { + missingSchemas.push(ref); + } + } + + // Check that all schemas are referenced in registry + const unregisteredSchemas = []; + for (const schemaId of actualSchemaIds) { + if (!registryRefs.has(schemaId)) { + unregisteredSchemas.push(schemaId); + } + } + + if (missingSchemas.length > 0) { + return `Registry references missing schemas: ${missingSchemas.join(', ')}`; + } + + if (unregisteredSchemas.length > 0) { + return `Unregistered schemas: ${unregisteredSchemas.join(', ')}`; + } + + return true; +} + +// Main test execution +async function runTests() { + log('🧪 Starting JSON Schema Validation Tests', 'info'); + log('=========================================='); + + // Find and load all schemas + const schemaPaths = findAllSchemas(SCHEMA_BASE_DIR); + const schemas = schemaPaths.map(schemaPath => [ + schemaPath, + loadSchema(schemaPath) + ]); + + log(`Found ${schemas.length} schemas to validate`); + + // Test 1: Validate each schema structure + await test('All schemas have required fields and valid structure', () => { + for (const [schemaPath, schema] of schemas) { + const result = validateSchemaStructure(schemaPath, schema); + if (result !== true) { + return `${path.basename(schemaPath)}: ${result}`; + } + } + return true; + }); + + // Test 2: Validate schema syntax with AJV + await test('All schemas are syntactically valid JSON Schema', async () => { + for (const [schemaPath, schema] of schemas) { + // Create a new AJV instance for each schema to avoid duplicate ID issues + const testAjv = new Ajv({ + allErrors: true, + verbose: true, + strict: false, + loadSchema: loadExternalSchema + }); + addFormats(testAjv); + + try { + await testAjv.compileAsync(schema); + } catch (error) { + return `${path.basename(schemaPath)}: ${error.message}`; + } + } + return true; + }); + + // Test 3: Validate cross-references + await test('All $ref cross-references resolve to existing schemas', () => { + return validateCrossReferences(schemas); + }); + + // Test 4: Validate registry consistency + await test('Schema registry is consistent with actual schemas', () => { + return validateRegistryConsistency(); + }); + + // Test 5: Validate enum schemas + await test('All enum schemas have proper enum values', () => { + const enumSchemas = schemas.filter(([path]) => path.includes('/enums/')); + + for (const [schemaPath, schema] of enumSchemas) { + if (!schema.enum || !Array.isArray(schema.enum) || schema.enum.length === 0) { + return `${path.basename(schemaPath)}: Missing or empty enum values`; + } + } + return true; + }); + + // Test 6: Validate required vs optional fields consistency + await test('Core schemas have appropriate required fields', () => { + const coreSchemas = schemas.filter(([path]) => path.includes('/core/')); + const requiredFieldChecks = { + 'product.json': ['product_id', 'name', 'description', 'formats', 'delivery_type', 'is_fixed_price'], + 'media-buy.json': ['media_buy_id', 'status', 'promoted_offering', 'total_budget', 'packages'], + 'package.json': ['package_id', 'status'], + 'creative-asset.json': ['creative_id', 'name', 'format'], + 'error.json': ['code', 'message'], + 'budget.json': ['total', 'currency'] + }; + + for (const [schemaPath, schema] of coreSchemas) { + const filename = path.basename(schemaPath); + const expectedRequired = requiredFieldChecks[filename]; + + if (expectedRequired) { + const actualRequired = schema.required || []; + const missing = expectedRequired.filter(field => !actualRequired.includes(field)); + + if (missing.length > 0) { + return `${filename}: Missing required fields: ${missing.join(', ')}`; + } + } + } + return true; + }); + + // Print results + log('\n=========================================='); + log(`Tests completed: ${totalTests}`); + log(`✅ Passed: ${passedTests}`, 'success'); + log(`❌ Failed: ${failedTests}`, failedTests > 0 ? 'error' : 'success'); + + if (failedTests > 0) { + process.exit(1); + } else { + log('\n🎉 All schema validation tests passed!', 'success'); + } +} + +// Run the tests +runTests().catch(error => { + log(`Test execution failed: ${error.message}`, 'error'); + process.exit(1); +}); \ No newline at end of file