# CVT (Contract Validator Toolkit) > Consumer-based contract validation platform for OpenAPI v2/v3 specifications. Validates HTTP request/response interactions against registered schemas via gRPC server with SDKs for Node.js, Python, Go, and Java, plus a CLI for local validation. CVT enables API contract testing by: - Validating HTTP interactions against OpenAPI schemas (v2/v3) - Detecting breaking changes between schema versions - Tracking consumer dependencies via consumer registry - Checking deployment safety with `can-i-deploy` - Generating test fixtures from schemas - Producer-side validation with framework middleware (Express, FastAPI, Gin, Spring, etc.) - HTTP client adapters for transparent contract validation (Axios, Fetch, Requests, OkHttp) ## Quick Start ### Node.js ```typescript import { ContractValidator } from "@sahina/cvt-sdk"; const validator = new ContractValidator("localhost:9550"); await validator.registerSchema("user-api", "./openapi.json"); const result = await validator.validate( { method: "GET", path: "/users/123", headers: {} }, { statusCode: 200, body: { id: "123", name: "John" } } ); if (!result.valid) { console.log("Validation errors:", result.errors); } validator.close(); ``` ### Python ```python from cvt_sdk import ContractValidator validator = ContractValidator("localhost:9550") validator.register_schema("user-api", "./openapi.json") result = validator.validate( request={"method": "GET", "path": "/users/123", "headers": {}}, response={"status_code": 200, "body": {"id": "123", "name": "John"}} ) if not result["valid"]: print("Validation errors:", result["errors"]) validator.close() ``` ### Go (gRPC SDK) ```go package main import ( "context" "fmt" "log" cvt "github.com/sahina/cvt/sdks/go/cvt" ) func main() { validator, err := cvt.NewValidator("localhost:9550") if err != nil { log.Fatal(err) } defer validator.Close() ctx := context.Background() err = validator.RegisterSchema(ctx, "user-api", "./openapi.json") if err != nil { log.Fatal(err) } result, err := validator.Validate(ctx, cvt.ValidationRequest{Method: "GET", Path: "/users/123"}, cvt.ValidationResponse{StatusCode: 200, Body: map[string]any{"id": "123", "name": "John"}}, ) if err != nil { log.Fatal(err) } if !result.Valid { fmt.Println("Errors:", result.Errors) } } ``` ### Go (Embedded Library) ```go package main import ( "fmt" "github.com/sahina/cvt/pkg/cvt" ) func main() { validator := cvt.NewValidator() err := validator.RegisterSchemaFromFile("user-api", "./openapi.json") if err != nil { panic(err) } result, err := validator.Validate("user-api", &cvt.Interaction{ Method: "GET", Path: "/users/123", StatusCode: 200, ResponseBody: `{"id":"123","name":"John"}`, }) if !result.Valid { fmt.Println("Errors:", result.Errors) } } ``` ### Java ```java import io.github.sahina.sdk.ContractValidator; import io.github.sahina.sdk.ValidationRequest; import io.github.sahina.sdk.ValidationResponse; import io.github.sahina.sdk.ValidationResult; ContractValidator validator = new ContractValidator("localhost:9550"); validator.registerSchema("user-api", "./openapi.json"); ValidationResult result = validator.validate( ValidationRequest.builder().method("GET").path("/users/123").build(), ValidationResponse.builder().statusCode(200).body("{\"id\":\"123\",\"name\":\"John\"}").build() ); if (!result.isValid()) { System.out.println("Errors: " + result.getErrors()); } validator.close(); ``` ### CLI ```bash # Validate interaction cvt validate --schema ./openapi.json --request req.json --response resp.json # Compare schemas for breaking changes cvt compare --old ./v1/openapi.json --new ./v2/openapi.json # Generate test fixtures cvt generate --schema ./openapi.json --method POST --path /users # Check deployment safety cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --server localhost:9550 # Wait for server readiness cvt wait --server localhost:9550 --timeout 120 # Register schema with server cvt register-schema my-api ./openapi.yaml --version 2.0.0 ``` ## Node.js SDK ### Installation ```bash npm install @sahina/cvt-sdk ``` ### Initialization ```typescript import { ContractValidator, ContractValidatorOptions, TLSOptions } from "@sahina/cvt-sdk"; // Simple connection const validator = new ContractValidator("localhost:9550"); // With TLS and API key const validator = new ContractValidator({ address: "localhost:9550", tls: { enabled: true, rootCertPath: "./certs/ca.crt" }, apiKey: "cvt-dev-key-12345" }); ``` ### Types ```typescript interface ValidationRequest { method: string; // GET, POST, PUT, DELETE, etc. path: string; // /users/123 headers?: Record; body?: TBody; } interface ValidationResponse { statusCode?: number; status_code?: number; // Alternative naming headers?: Record; body?: TBody; } interface ValidationResult { valid: boolean; errors?: string[]; } interface BreakingChange { type: string; // ENDPOINT_REMOVED, REQUIRED_FIELD_ADDED, TYPE_CHANGED, etc. path: string; // /users/{id} method: string; // DELETE description: string; oldValue?: string; newValue?: string; } interface EndpointUsage { method: string; path: string; usedFields?: string[]; // ["email", "name"] } interface RegisterConsumerOptions { consumerId: string; consumerVersion: string; schemaId: string; schemaVersion: string; environment: string; // dev, staging, prod usedEndpoints?: EndpointUsage[]; } interface ConsumerImpact { consumerId: string; consumerVersion: string; currentSchemaVersion: string; // Version consumer was tested against environment: string; willBreak: boolean; // True if consumer will be affected relevantChanges: BreakingChange[]; // Breaking changes affecting this consumer } interface CanIDeployResult { safeToDeploy: boolean; summary: string; breakingChanges: BreakingChange[]; affectedConsumers: ConsumerImpact[]; } ``` ### Methods #### registerSchema(schemaId, schemaPath) Registers an OpenAPI schema from file path or URL. ```typescript await validator.registerSchema("user-api", "./openapi.json"); await validator.registerSchema("user-api", "https://api.example.com/openapi.json"); ``` #### registerSchemaWithVersion(schemaId, schemaPath, version) Registers schema with semantic version for comparison. ```typescript await validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0"); ``` #### validate(request, response) Validates HTTP interaction against registered schema. ```typescript const result = await validator.validate( { method: "POST", path: "/users", headers: {"Content-Type": "application/json"}, body: { name: "John" } }, { statusCode: 201, body: { id: "1", name: "John" } } ); // result: { valid: true, errors: [] } ``` #### compareSchemas(schemaId, oldVersion?, newVersion?) Compares two schema versions for breaking changes. ```typescript const result = await validator.compareSchemas("user-api", "1.0.0", "2.0.0"); if (!result.compatible) { result.breakingChanges.forEach(change => { console.log(`${change.type}: ${change.description}`); }); } ``` #### generateFixture(method, path, options?) Generates test fixture from schema. ```typescript const fixture = await validator.generateFixture("POST", "/users", { useExamples: true }); // fixture: { request: { method, path, headers, body }, response: { statusCode, headers, body } } ``` #### generateResponse(method, path, options?) Generates response fixture only. ```typescript const response = await validator.generateResponse("GET", "/users/123"); // response: { statusCode: 200, headers: {}, body: { id: "123", name: "..." } } ``` #### generateRequestBody(method, path, options?) Generates request body only. ```typescript const body = await validator.generateRequestBody("POST", "/users"); // body: { name: "string", email: "string" } ``` #### listEndpoints() Lists all endpoints in registered schema. ```typescript const endpoints = await validator.listEndpoints(); // endpoints: [{ method: "GET", path: "/users", operationId: "getUsers", summary: "List users" }, ...] ``` #### registerConsumer(options) Registers consumer dependency on schema. ```typescript const consumer = await validator.registerConsumer({ consumerId: "order-service", consumerVersion: "2.1.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "prod", usedEndpoints: [ { method: "GET", path: "/users/{id}", usedFields: ["email", "name"] } ] }); ``` #### listConsumers(schemaId, environment?) Lists consumers depending on a schema. ```typescript const consumers = await validator.listConsumers("user-api", "prod"); ``` #### deregisterConsumer(consumerId, schemaId, environment) Removes consumer registration. ```typescript await validator.deregisterConsumer("order-service", "user-api", "prod"); ``` #### canIDeploy(schemaId, newVersion, environment) Checks if schema can be safely deployed. ```typescript const result = await validator.canIDeploy("user-api", "2.0.0", "prod"); if (!result.safeToDeploy) { console.log(result.summary); result.affectedConsumers.forEach(c => { if (c.willBreak) console.log(`${c.consumerId} will break!`); }); } ``` #### close() Closes gRPC connection. ```typescript validator.close(); ``` #### buildConsumerFromInteractions(interactions, options) Builds consumer registration from recorded interactions. ```typescript const consumerData = validator.buildConsumerFromInteractions( [ { request: { method: "GET", path: "/users/123" }, response: { statusCode: 200, body: { id: "123" } } } ], { consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", environment: "dev" } ); ``` #### registerConsumerFromInteractions(interactions, options) Registers consumer directly from recorded interactions. ```typescript await validator.registerConsumerFromInteractions( [ { request: { method: "GET", path: "/users/123" }, response: { statusCode: 200, body: { id: "123" } } } ], { consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev" } ); ``` ### Node.js Adapters (HTTP Client Integration) Wrap HTTP clients for automatic contract validation. Captured interactions can be used for consumer auto-registration. #### Axios Adapter ```typescript import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters"; import axios from "axios"; const api = axios.create({ baseURL: "http://localhost:3000" }); const adapter = createAxiosAdapter({ validator, axios: api, autoValidate: true, // validate every request (default: true) includePaths: ["/users/**"], // only validate matching paths excludePaths: ["/health"], // skip these paths onValidationFailure: (result, req, res) => { console.error("Contract violation:", result.errors); }, }); // Use axios normally — validation happens automatically const response = await api.get("/users/123"); // Get captured interactions for consumer registration const interactions = adapter.getInteractions(); adapter.clearInteractions(); // Detach adapter when done adapter.detach(); ``` #### Fetch Adapter ```typescript import { createValidatingFetch } from "@sahina/cvt-sdk/adapters"; const { fetch: validatedFetch, adapter } = createValidatingFetch({ validator, baseURL: "http://localhost:3000", autoValidate: true, includePaths: ["/users/**"], }); // Use like standard fetch const response = await validatedFetch("/users/123"); // Get captured interactions const interactions = adapter.getInteractions(); adapter.clearInteractions(); ``` #### Mock Adapter Generate schema-compliant mock responses for consumer testing without a real API. ```typescript import { createMockFetch, createMockAdapter } from "@sahina/cvt-sdk/adapters"; // Simple mock fetch function const mockFetch = createMockFetch(validator, { cache: true }); const response = await mockFetch("/users/123"); const data = await response.json(); // Full mock adapter with more control const mock = createMockAdapter({ validator, cache: true, generateOptions: { useExamples: true }, includePaths: ["/users/**"], }); const response = await mock.fetch("/users/123"); const interactions = mock.getInteractions(); mock.clearCache(); ``` ### Node.js Producer Validation (Server-Side) Validate your API server's requests and responses against its OpenAPI schema. **Validation Modes:** - `strict` — Reject invalid requests with 400 errors; log response violations - `warn` — Log all violations but allow requests to proceed - `shadow` — Validate asynchronously, record metrics only (for production rollout) #### Express Middleware ```typescript import { createExpressMiddleware } from "@sahina/cvt-sdk/producer"; const middleware = createExpressMiddleware({ schemaId: "user-api", validator, mode: "strict", validateRequest: true, validateResponse: true, includePaths: ["/api/**"], excludePaths: ["/api/health"], }); app.use(middleware); ``` #### Fastify Plugin ```typescript import { fastifyProducerPlugin } from "@sahina/cvt-sdk/producer"; await app.register(fastifyProducerPlugin, { schemaId: "user-api", validator, mode: "warn", logPrefix: "cvt", }); ``` #### ProducerTestKit Test your API responses against the schema without running middleware. ```typescript import { ProducerTestKit } from "@sahina/cvt-sdk/producer"; const testKit = new ProducerTestKit({ schemaId: "user-api", serverAddress: "localhost:9550", }); // Validate a single response const result = await testKit.validateResponse({ method: "GET", path: "/users/123", response: { statusCode: 200, body: { id: "123", name: "John" } }, }); expect(result.valid).toBe(true); // Validate full interaction const result = await testKit.validateInteraction({ request: { method: "POST", path: "/users", body: { name: "Jane" } }, response: { statusCode: 201, body: { id: "2", name: "Jane" } }, }); // Reusable endpoint tester const userEndpoint = testKit.forEndpoint("GET", "/users/{id}"); const r1 = await userEndpoint.validateResponse( { statusCode: 200, body: { id: "1", name: "John" } }, { pathValues: { id: "1" } } ); testKit.close(); ``` #### Metrics ```typescript import { getMetrics, resetMetrics } from "@sahina/cvt-sdk/producer"; const metrics = getMetrics(); // { // requestValidations, requestValidationsPassed, requestValidationsFailed, // responseValidations, responseValidationsPassed, responseValidationsFailed, // requestsRejected // } resetMetrics(); ``` ## Python SDK ### Installation ```bash pip install cvt-sdk ``` ### Initialization ```python from cvt_sdk import ContractValidator, ContractValidatorOptions, TLSOptions # Simple connection validator = ContractValidator("localhost:9550") # With TLS and API key validator = ContractValidator(ContractValidatorOptions( address="localhost:9550", tls=TLSOptions(enabled=True, root_cert_path="./certs/ca.crt"), api_key="cvt-dev-key-12345" )) ``` ### Types ```python from cvt_sdk import ( ValidationRequest, # TypedDict: method, path, headers, body ValidationResponse, # TypedDict: status_code, headers, body ValidationResult, # TypedDict: valid, errors BreakingChange, # TypedDict: type, path, method, description GenerateOptions, # dataclass: status_code, use_examples, content_type EndpointUsage, # dataclass: method, path, used_fields RegisterConsumerOptions,# dataclass: consumer_id, consumer_version, schema_id, etc. CanIDeployResult, # TypedDict: safe_to_deploy, summary, breaking_changes, affected_consumers ) ``` ### Methods #### register_schema(schema_id, schema_path) ```python validator.register_schema("user-api", "./openapi.json") validator.register_schema("user-api", "https://api.example.com/openapi.json") ``` #### register_schema_with_version(schema_id, schema_path, version) ```python validator.register_schema_with_version("user-api", "./openapi.json", "1.0.0") ``` #### validate(request, response) ```python result = validator.validate( request={"method": "POST", "path": "/users", "body": {"name": "John"}}, response={"status_code": 201, "body": {"id": "1", "name": "John"}} ) # result: {"valid": True, "errors": []} ``` #### compare_schemas(schema_id, old_version="", new_version="") ```python result = validator.compare_schemas("user-api", "1.0.0", "2.0.0") if not result["compatible"]: for change in result["breaking_changes"]: print(f"{change['type']}: {change['description']}") ``` #### generate_fixture(method, path, options=None) ```python from cvt_sdk import GenerateOptions fixture = validator.generate_fixture("POST", "/users", GenerateOptions(use_examples=True)) # fixture: {"request": {...}, "response": {...}} ``` #### generate_response(method, path, options=None) ```python response = validator.generate_response("GET", "/users/123") # response: {"status_code": 200, "headers": {}, "body": {...}} ``` #### generate_request_body(method, path, options=None) ```python body = validator.generate_request_body("POST", "/users") # body: {"name": "string", "email": "string"} ``` #### list_endpoints() ```python endpoints = validator.list_endpoints() # endpoints: [{"method": "GET", "path": "/users", ...}, ...] ``` #### register_consumer(options) ```python from cvt_sdk import RegisterConsumerOptions, EndpointUsage consumer = validator.register_consumer(RegisterConsumerOptions( consumer_id="order-service", consumer_version="2.1.0", schema_id="user-api", schema_version="1.0.0", environment="prod", used_endpoints=[ EndpointUsage(method="GET", path="/users/{id}", used_fields=["email", "name"]) ] )) ``` #### list_consumers(schema_id, environment=None) ```python consumers = validator.list_consumers("user-api", "prod") ``` #### deregister_consumer(consumer_id, schema_id, environment) ```python validator.deregister_consumer("order-service", "user-api", "prod") ``` #### can_i_deploy(schema_id, new_version, environment) ```python result = validator.can_i_deploy("user-api", "2.0.0", "prod") if not result["safe_to_deploy"]: print(result["summary"]) ``` #### close() ```python validator.close() ``` #### build_consumer_from_interactions(interactions, options) Builds consumer registration from recorded interactions. ```python consumer_data = validator.build_consumer_from_interactions( interactions=[ {"request": {"method": "GET", "path": "/users/123"}, "response": {"status_code": 200, "body": {"id": "123"}}} ], options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "environment": "dev"} ) ``` #### register_consumer_from_interactions(interactions, options) Registers consumer directly from recorded interactions. ```python validator.register_consumer_from_interactions( interactions=[ {"request": {"method": "GET", "path": "/users/123"}, "response": {"status_code": 200, "body": {"id": "123"}}} ], options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "schema_version": "1.0.0", "environment": "dev"} ) ``` ### Python Adapters (HTTP Client Integration) #### Requests Session (ContractValidatingSession) Drop-in replacement for `requests.Session` that validates every HTTP interaction. ```python from cvt_sdk.adapters import ContractValidatingSession, create_validating_session # Using factory function session = create_validating_session( validator, auto_validate=True, include_paths=["/users/**"], exclude_paths=["/health"], on_validation_failure=lambda result, req, res: print(f"Violation: {result}"), ) # Use like a normal requests session response = session.get("http://localhost:3000/users/123") response = session.post("http://localhost:3000/users", json={"name": "Jane"}) # Get captured interactions for consumer registration interactions = session.get_interactions() session.clear_interactions() # Validate a specific interaction result = session.validate_interaction(interactions[0]) ``` #### Mock Session Generate schema-compliant mock responses for consumer testing without a real API. ```python from cvt_sdk.adapters import MockSession, create_mock_session mock = create_mock_session( validator, cache=True, generate_options=GenerateOptions(use_examples=True), include_paths=["/users/**"], ) # HTTP methods work like requests.Session response = mock.get("/users/123") data = response.json() response = mock.post("/users", json_body={"name": "Jane"}) # Get captured interactions interactions = mock.get_interactions() mock.clear_cache() ``` ### Python Producer Validation (Server-Side) **Validation Modes:** - `ValidationMode.STRICT` — Reject invalid requests with error responses - `ValidationMode.WARN` — Log violations but allow requests to proceed - `ValidationMode.SHADOW` — Validate asynchronously, record metrics only #### FastAPI / ASGI Middleware ```python from cvt_sdk.producer import ProducerConfig, ValidationMode from cvt_sdk.producer.adapters import ASGIMiddleware, create_fastapi_middleware app = FastAPI() config = ProducerConfig( schema_id="user-api", validator=validator, mode=ValidationMode.STRICT, validate_request=True, validate_response=True, include_paths=["/api/.*"], exclude_paths=["/api/health"], ) app.add_middleware(ASGIMiddleware, config=config) # Or use factory function MiddlewareClass = create_fastapi_middleware(config) app.add_middleware(MiddlewareClass) ``` #### Flask / WSGI Middleware ```python from cvt_sdk.producer import ProducerConfig, ValidationMode from cvt_sdk.producer.adapters import WSGIMiddleware, create_flask_middleware app = Flask(__name__) config = ProducerConfig( schema_id="user-api", validator=validator, mode=ValidationMode.WARN, ) app.wsgi_app = WSGIMiddleware(app.wsgi_app, config=config) # Or use factory function middleware_factory = create_flask_middleware(config) app.wsgi_app = middleware_factory(app.wsgi_app) ``` #### ProducerTestKit ```python from cvt_sdk.producer import ProducerTestKit, ProducerTestConfig, MockResponse, MockRequest test_kit = ProducerTestKit(ProducerTestConfig( schema_id="user-api", server_address="localhost:9550", )) # Validate a response result = test_kit.validate_response( method="GET", path="/users/123", status_code=200, body={"id": "123", "name": "John"}, ) assert result.valid # Validate a full interaction result = test_kit.validate_interaction( request=MockRequest(method="POST", path="/users", body={"name": "Jane"}), response=MockResponse(status_code=201, body={"id": "2", "name": "Jane"}), ) # Reusable endpoint tester user_endpoint = test_kit.for_endpoint("GET", "/users/{id}") result = user_endpoint.validate_response( status_code=200, body={"id": "1", "name": "John"}, path_values={"id": "1"}, ) test_kit.close() ``` #### Metrics ```python from cvt_sdk.producer import get_metrics, reset_metrics metrics = get_metrics() # { # "request_validations", "request_validations_passed", "request_validations_failed", # "response_validations", "response_validations_passed", "response_validations_failed", # "requests_rejected" # } reset_metrics() ``` ## Go gRPC SDK Full gRPC client SDK with all RPC methods, TLS, and API key authentication. ### Installation ```bash go get github.com/sahina/cvt/sdks/go ``` ```go import cvt "github.com/sahina/cvt/sdks/go/cvt" ``` ### Initialization ```go // Simple connection validator, err := cvt.NewValidator("localhost:9550") if err != nil { log.Fatal(err) } defer validator.Close() // With TLS and API key validator, err := cvt.NewValidatorWithOptions(cvt.ValidatorOptions{ Address: "localhost:9550", TLS: &cvt.TLSOptions{ Enabled: true, RootCertPath: "./certs/ca.crt", CertPath: "./certs/client.crt", // For mTLS KeyPath: "./certs/client.key", // For mTLS }, APIKey: "cvt-dev-key-12345", }) ``` ### Types ```go type ValidatorOptions struct { Address string TLS *TLSOptions APIKey string } type TLSOptions struct { Enabled bool RootCertPath string CertPath string // Client cert for mTLS KeyPath string // Client key for mTLS } type ValidationRequest struct { Method string Path string Headers map[string]string Body any } type ValidationResponse struct { StatusCode int Headers map[string]string Body any } type ValidationResult struct { Valid bool Errors []string } type CompareResult struct { Compatible bool BreakingChanges []BreakingChange } type BreakingChange struct { Type string Path string Method string Description string OldValue string NewValue string } type GenerateOptions struct { StatusCode int UseExamples bool ContentType string } type GeneratedFixture struct { Request *GeneratedRequest Response *GeneratedResponse } type GeneratedRequest struct { Method string Path string Headers map[string]string Body any } type GeneratedResponse struct { StatusCode int Headers map[string]string Body any } type EndpointInfo struct { Method string Path string OperationID string Summary string } type EndpointUsage struct { Method string Path string UsedFields []string } type RegisterConsumerOptions struct { ConsumerID string ConsumerVersion string SchemaID string SchemaVersion string Environment string UsedEndpoints []EndpointUsage } type ConsumerInfo struct { ConsumerID string ConsumerVersion string SchemaID string SchemaVersion string Environment string RegisteredAt int64 LastValidatedAt int64 UsedEndpoints []EndpointUsage } type ConsumerImpact struct { ConsumerID string ConsumerVersion string CurrentSchemaVersion string Environment string WillBreak bool RelevantChanges []BreakingChange } type CanIDeployResult struct { SafeToDeploy bool Summary string BreakingChanges []BreakingChange AffectedConsumers []ConsumerImpact } type CapturedInteraction struct { Request ValidationRequest Response ValidationResponse ValidationResult *ValidationResult Timestamp time.Time } type AutoRegisterConfig struct { ConsumerID string ConsumerVersion string Environment string SchemaVersion string SchemaID string // optional, auto-extracted if empty } ``` ### Methods All methods take a `context.Context` as the first parameter. #### RegisterSchema(ctx, schemaID, schemaPath) error ```go err := validator.RegisterSchema(ctx, "user-api", "./openapi.json") err := validator.RegisterSchema(ctx, "user-api", "https://api.example.com/openapi.json") ``` #### RegisterSchemaWithVersion(ctx, schemaID, schemaPath, version) error ```go err := validator.RegisterSchemaWithVersion(ctx, "user-api", "./openapi.json", "1.0.0") ``` #### Validate(ctx, request, response) (*ValidationResult, error) ```go result, err := validator.Validate(ctx, cvt.ValidationRequest{ Method: "POST", Path: "/users", Headers: map[string]string{"Content-Type": "application/json"}, Body: map[string]any{"name": "John"}, }, cvt.ValidationResponse{ StatusCode: 201, Body: map[string]any{"id": "1", "name": "John"}, }, ) if !result.Valid { fmt.Println("Errors:", result.Errors) } ``` #### CompareSchemas(ctx, schemaID, oldVersion, newVersion) (*CompareResult, error) ```go result, err := validator.CompareSchemas(ctx, "user-api", "1.0.0", "2.0.0") if !result.Compatible { for _, change := range result.BreakingChanges { fmt.Printf("%s: %s\n", change.Type, change.Description) } } ``` #### GenerateFixture(ctx, method, path, opts) (*GeneratedFixture, error) ```go fixture, err := validator.GenerateFixture(ctx, "POST", "/users", &cvt.GenerateOptions{UseExamples: true}) fmt.Println("Request body:", fixture.Request.Body) fmt.Println("Response:", fixture.Response.StatusCode, fixture.Response.Body) ``` #### GenerateResponse(ctx, method, path, opts) (*GeneratedResponse, error) ```go response, err := validator.GenerateResponse(ctx, "GET", "/users/123", nil) fmt.Println("Status:", response.StatusCode, "Body:", response.Body) ``` #### GenerateRequestBody(ctx, method, path, opts) (any, error) ```go body, err := validator.GenerateRequestBody(ctx, "POST", "/users", nil) ``` #### ListEndpoints(ctx) ([]EndpointInfo, error) ```go endpoints, err := validator.ListEndpoints(ctx) for _, ep := range endpoints { fmt.Printf("%s %s - %s\n", ep.Method, ep.Path, ep.Summary) } ``` #### RegisterConsumer(ctx, opts) (*ConsumerInfo, error) ```go consumer, err := validator.RegisterConsumer(ctx, cvt.RegisterConsumerOptions{ ConsumerID: "order-service", ConsumerVersion: "2.1.0", SchemaID: "user-api", SchemaVersion: "1.0.0", Environment: "prod", UsedEndpoints: []cvt.EndpointUsage{ {Method: "GET", Path: "/users/{id}", UsedFields: []string{"email", "name"}}, }, }) ``` #### ListConsumers(ctx, schemaID, environment) ([]ConsumerInfo, error) ```go consumers, err := validator.ListConsumers(ctx, "user-api", "prod") ``` #### DeregisterConsumer(ctx, consumerID, schemaID, environment) error ```go err := validator.DeregisterConsumer(ctx, "order-service", "user-api", "prod") ``` #### CanIDeploy(ctx, schemaID, newVersion, environment) (*CanIDeployResult, error) ```go result, err := validator.CanIDeploy(ctx, "user-api", "2.0.0", "prod") if !result.SafeToDeploy { fmt.Println(result.Summary) for _, c := range result.AffectedConsumers { if c.WillBreak { fmt.Printf("%s will break!\n", c.ConsumerID) } } } ``` #### BuildConsumerFromInteractions(ctx, interactions, config) (*RegisterConsumerOptions, error) ```go opts, err := validator.BuildConsumerFromInteractions(ctx, interactions, cvt.AutoRegisterConfig{ ConsumerID: "order-service", ConsumerVersion: "1.0.0", Environment: "dev", SchemaVersion: "1.0.0", }) ``` #### RegisterConsumerFromInteractions(ctx, interactions, config) (*ConsumerInfo, error) ```go consumer, err := validator.RegisterConsumerFromInteractions(ctx, interactions, cvt.AutoRegisterConfig{ ConsumerID: "order-service", ConsumerVersion: "1.0.0", Environment: "dev", SchemaVersion: "1.0.0", SchemaID: "user-api", }) ``` #### Close() error ```go err := validator.Close() ``` ### Go Adapters (HTTP Client Integration) #### ValidatingRoundTripper Wraps `http.RoundTripper` for transparent client-side validation. ```go import "github.com/sahina/cvt/sdks/go/cvt/adapters" rt := adapters.NewValidatingRoundTripper(adapters.RoundTripperConfig{ Validator: validator, Transport: http.DefaultTransport, AutoValidate: true, IncludePaths: []adapters.PathFilter{"/users/**"}, ExcludePaths: []adapters.PathFilter{"/health"}, OnValidationFailure: func(result *cvt.ValidationResult, req *http.Request, resp *http.Response) error { log.Printf("Contract violation on %s %s: %v", req.Method, req.URL.Path, result.Errors) return nil // return error to fail the request }, }) client := &http.Client{Transport: rt} resp, err := client.Get("http://localhost:3000/users/123") // Get captured interactions for consumer registration interactions := rt.GetInteractions() rt.ClearInteractions() ``` #### ValidatingMiddleware (Server-Side) ```go middleware := adapters.NewValidatingMiddleware(adapters.MiddlewareConfig{ Validator: validator, AutoValidate: true, IncludePaths: []adapters.PathFilter{"/api/**"}, }) mux := http.NewServeMux() mux.Handle("/api/users", middleware.Handler(userHandler)) // or mux.Handle("/api/users", middleware.HandlerFunc(userHandlerFunc)) interactions := middleware.GetInteractions() ``` #### Mock Client Generate schema-compliant mock responses for testing. ```go import "github.com/sahina/cvt/sdks/go/cvt/adapters" // Simple mock client mockClient := adapters.NewMockClient(validator, adapters.WithCache(), adapters.WithRequestValidation(), adapters.WithGenerateOptions(&cvt.GenerateOptions{UseExamples: true}), ) resp, err := mockClient.Get("http://api.example.com/users/123") // Full control with Mock wrapper mock := adapters.NewMock(validator, adapters.WithCache()) client := mock.Client() resp, err := client.Get("http://api.example.com/users/123") interactions := mock.GetInteractions() mock.ClearCache() ``` ### Go Producer Validation (Server-Side) ```go import ( "github.com/sahina/cvt/sdks/go/cvt/producer" producerAdapters "github.com/sahina/cvt/sdks/go/cvt/producer/adapters" ) ``` **Validation Modes:** - `producer.ModeStrict` — Reject invalid requests - `producer.ModeWarn` — Log violations, allow requests - `producer.ModeShadow` — Async validation, metrics only #### net/http Middleware ```go middleware := producerAdapters.NetHTTPMiddleware(producer.Config{ SchemaID: "user-api", Validator: validator, Mode: producer.ModeStrict, ValidateRequest: true, ValidateResponse: true, IncludePaths: []producer.PathFilter{"/api/.*"}, ExcludePaths: []producer.PathFilter{"/api/health"}, }) mux := http.NewServeMux() handler := middleware(mux) http.ListenAndServe(":8080", handler) ``` #### Chi Middleware ```go r := chi.NewRouter() r.Use(producerAdapters.ChiMiddleware(producer.Config{ SchemaID: "user-api", Validator: validator, Mode: producer.ModeWarn, })) ``` #### Gin Middleware ```go r := gin.Default() r.Use(producerAdapters.GinMiddleware(producer.Config{ SchemaID: "user-api", Validator: validator, Mode: producer.ModeShadow, })) ``` #### ProducerTestKit ```go import "github.com/sahina/cvt/sdks/go/cvt/producer" testKit, err := producer.NewProducerTestKit(producer.TestConfig{ SchemaID: "user-api", ServerAddress: "localhost:9550", }) defer testKit.Close() // Validate a response result, err := testKit.ValidateResponse(ctx, producer.ValidateResponseParams{ Method: "GET", Path: "/users/123", Response: producer.TestResponseData{StatusCode: 200, Body: map[string]any{"id": "123", "name": "John"}}, }) // Validate full interaction result, err := testKit.ValidateInteraction(ctx, producer.TestRequestContext{Method: "POST", Path: "/users", Body: map[string]any{"name": "Jane"}}, producer.TestResponseData{StatusCode: 201, Body: map[string]any{"id": "2", "name": "Jane"}}, ) // Reusable endpoint tester userEndpoint := testKit.ForEndpoint("GET", "/users/{id}") result, err := userEndpoint.ValidateResponse(ctx, producer.TestResponseData{StatusCode: 200, Body: map[string]any{"id": "1"}}, map[string]string{"id": "1"}, ) ``` #### Metrics ```go metrics := producer.GetMetrics() snapshot := metrics.Snapshot() fmt.Println("Request validations:", snapshot.RequestValidations) fmt.Println("Failed:", snapshot.RequestValidationsFailed) producer.ResetMetrics() ``` ## Go Embedded Library The embedded library allows local validation without a gRPC server. ### Installation ```go import "github.com/sahina/cvt/pkg/cvt" ``` ### Types ```go type Interaction struct { Method string `json:"method"` // GET, POST, etc. Path string `json:"path"` // /users/123 Headers map[string]string `json:"headers,omitempty"` Body string `json:"body,omitempty"` // JSON string StatusCode int `json:"status_code"` ResponseHeaders map[string]string `json:"response_headers,omitempty"` ResponseBody string `json:"response_body"` // JSON string } type ValidationResult struct { Valid bool `json:"valid"` Errors []string `json:"errors,omitempty"` } ``` ### Methods #### NewValidator() ```go validator := cvt.NewValidator() ``` #### RegisterSchema(schemaID string, content []byte) error ```go content, _ := os.ReadFile("./openapi.json") err := validator.RegisterSchema("user-api", content) ``` #### RegisterSchemaFromFile(schemaID, filePath string) error ```go err := validator.RegisterSchemaFromFile("user-api", "./openapi.json") ``` #### Validate(schemaID string, interaction *Interaction) (*ValidationResult, error) ```go result, err := validator.Validate("user-api", &cvt.Interaction{ Method: "GET", Path: "/users/123", Headers: map[string]string{"Accept": "application/json"}, StatusCode: 200, ResponseBody: `{"id":"123","name":"John","email":"john@example.com"}`, }) if !result.Valid { fmt.Println("Errors:", result.Errors) } ``` #### ValidateWithSchema(schemaContent []byte, interaction *Interaction) (*ValidationResult, error) ```go schemaContent, _ := os.ReadFile("./openapi.json") result, err := validator.ValidateWithSchema(schemaContent, interaction) ``` #### ListSchemas() []string ```go schemas := validator.ListSchemas() ``` #### RemoveSchema(schemaID string) ```go validator.RemoveSchema("user-api") ``` #### GetSchema(schemaID string) (*openapi3.T, bool) ```go doc, found := validator.GetSchema("user-api") ``` ## Java SDK ### Installation Maven: ```xml io.github.sahina cvt-sdk 0.1.0 ``` Gradle: ```gradle dependencies { implementation 'io.github.sahina:cvt-sdk:0.1.0' } ``` ### Initialization ```java import io.github.sahina.sdk.ContractValidator; // Simple connection ContractValidator validator = new ContractValidator("localhost:9550"); // With TLS and API key ContractValidator validator = ContractValidator.builder() .address("localhost:9550") .tlsEnabled(true) .rootCertPath("./certs/ca.crt") .apiKey("cvt-dev-key-12345") .build(); ``` ### Types ```java import io.github.sahina.sdk.ValidationRequest; import io.github.sahina.sdk.ValidationResponse; import io.github.sahina.sdk.ValidationResult; import io.github.sahina.sdk.CompareResult; import io.github.sahina.sdk.BreakingChange; import io.github.sahina.sdk.GenerateOptions; import io.github.sahina.sdk.GeneratedFixture; import io.github.sahina.sdk.GeneratedResponse; import io.github.sahina.sdk.EndpointInfo; import io.github.sahina.sdk.EndpointUsage; import io.github.sahina.sdk.RegisterConsumerOptions; import io.github.sahina.sdk.ConsumerInfo; import io.github.sahina.sdk.CanIDeployResult; import io.github.sahina.sdk.ConsumerImpact; // Validation types ValidationRequest request = ValidationRequest.builder() .method("GET") .path("/users/123") .headers(Map.of("Content-Type", "application/json")) .build(); ValidationResponse response = ValidationResponse.builder() .statusCode(200) .body("{\"id\": \"123\", \"name\": \"John\"}") .build(); ValidationResult result = validator.validate(request, response); // result.isValid(), result.getErrors() // Breaking change types BreakingChange change; // type, path, method, description, oldValue, newValue CompareResult compareResult; // compatible, breakingChanges // Consumer registry types RegisterConsumerOptions options; // consumerId, consumerVersion, schemaId, schemaVersion, environment, usedEndpoints ConsumerInfo info; // consumerId, consumerVersion, schemaId, schemaVersion, environment, registeredAt, lastValidatedAt, usedEndpoints CanIDeployResult deployResult; // safeToDeploy, summary, breakingChanges, affectedConsumers ``` ### Methods #### registerSchema(schemaId, schemaPath) ```java validator.registerSchema("user-api", "./openapi.json"); validator.registerSchema("user-api", "https://api.example.com/openapi.json"); ``` #### registerSchemaWithVersion(schemaId, schemaPath, version) ```java validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0"); ``` #### validate(request, response) ```java ValidationResult result = validator.validate( ValidationRequest.builder() .method("POST") .path("/users") .body("{\"name\": \"John\"}") .build(), ValidationResponse.builder() .statusCode(201) .body("{\"id\": \"1\", \"name\": \"John\"}") .build() ); // result: ValidationResult(valid=true, errors=[]) ``` #### compareSchemas(schemaId, oldVersion, newVersion) ```java CompareResult result = validator.compareSchemas("user-api", "1.0.0", "2.0.0"); if (!result.isCompatible()) { for (BreakingChange change : result.getBreakingChanges()) { System.out.println(change.getType() + ": " + change.getDescription()); } } ``` #### generateFixture(method, path, options) ```java GeneratedFixture fixture = validator.generateFixture("POST", "/users", GenerateOptions.builder().useExamples(true).build()); // fixture: GeneratedFixture(request, response) ``` #### generateResponse(method, path, options) ```java GeneratedResponse response = validator.generateResponse("GET", "/users/123", null); // response: GeneratedResponse(statusCode, headers, body) ``` #### generateRequestBody(method, path, options) ```java Object body = validator.generateRequestBody("POST", "/users", null); // body: Map or List depending on schema ``` #### listEndpoints() ```java List endpoints = validator.listEndpoints(); for (EndpointInfo ep : endpoints) { System.out.println(ep.getMethod() + " " + ep.getPath()); } ``` #### registerConsumer(options) ```java ConsumerInfo consumer = validator.registerConsumer(RegisterConsumerOptions.builder() .consumerId("order-service") .consumerVersion("2.1.0") .schemaId("user-api") .schemaVersion("1.0.0") .environment("prod") .usedEndpoints(List.of( new EndpointUsage("GET", "/users/{id}", List.of("email", "name")) )) .build()); ``` #### listConsumers(schemaId, environment) ```java List consumers = validator.listConsumers("user-api", "prod"); ``` #### deregisterConsumer(consumerId, schemaId, environment) ```java validator.deregisterConsumer("order-service", "user-api", "prod"); ``` #### canIDeploy(schemaId, newVersion, environment) ```java CanIDeployResult result = validator.canIDeploy("user-api", "2.0.0", "prod"); if (!result.isSafeToDeploy()) { System.out.println(result.getSummary()); for (ConsumerImpact c : result.getAffectedConsumers()) { if (c.isWillBreak()) { System.out.println(c.getConsumerId() + " will break!"); } } } ``` #### close() ```java validator.close(); // Or use try-with-resources since ContractValidator implements AutoCloseable try (ContractValidator validator = new ContractValidator("localhost:9550")) { // use validator } ``` #### buildConsumerFromInteractions(interactions, options) Builds consumer registration from recorded interactions. ```java import io.github.sahina.sdk.adapters.CapturedInteraction; import io.github.sahina.sdk.AutoRegisterConfig; AutoRegisterUtils.BuildResult consumerData = validator.buildConsumerFromInteractions( List.of(new CapturedInteraction("GET", "/users/123", Map.of(), null, 200, Map.of(), "{\"id\":\"123\"}", null)), AutoRegisterConfig.builder() .consumerId("order-service") .consumerVersion("1.0.0") .schemaId("user-api") .environment("dev") .build() ); ``` #### registerConsumerFromInteractions(interactions, options) Registers consumer directly from recorded interactions. ```java ConsumerInfo consumer = validator.registerConsumerFromInteractions( List.of(new CapturedInteraction("GET", "/users/123", Map.of(), null, 200, Map.of(), "{\"id\":\"123\"}", null)), AutoRegisterConfig.builder() .consumerId("order-service") .consumerVersion("1.0.0") .schemaId("user-api") .schemaVersion("1.0.0") .environment("dev") .build() ); ``` ### Java Adapters (HTTP Client Integration) #### OkHttp Adapter Intercepts OkHttp requests for automatic contract validation. ```java import io.github.sahina.sdk.adapters.OkHttpContractAdapter; import io.github.sahina.sdk.adapters.AdapterConfig; import io.github.sahina.sdk.adapters.CapturedInteraction; OkHttpContractAdapter adapter = new OkHttpContractAdapter(validator) .withConfig(AdapterConfig.builder() .autoValidate(true) .includePath("/users/.*") .excludePath("/health") .captureInteractions(true) .build()); OkHttpClient client = adapter.attach(new OkHttpClient.Builder()).build(); // Use client normally — validation happens automatically Request request = new Request.Builder().url("http://localhost:3000/users/123").build(); Response response = client.newCall(request).execute(); // Get captured interactions for consumer registration List interactions = adapter.getInteractions(); List invalid = adapter.getInvalidInteractions(); adapter.clearInteractions(); ``` #### Mock Interceptor Generate schema-compliant mock responses for testing. ```java import io.github.sahina.sdk.adapters.MockInterceptor; import io.github.sahina.sdk.adapters.MockConfig; // Simple mock client OkHttpClient mockClient = MockInterceptor.createMockClient(validator); // With configuration OkHttpClient mockClient = MockInterceptor.createMockClient(validator, MockConfig.builder() .cache(true) .generateOptions(GenerateOptions.builder().useExamples(true).build()) .includePath("/users/.*") .build()); Request request = new Request.Builder().url("http://api.example.com/users/123").build(); Response response = mockClient.newCall(request).execute(); String body = response.body().string(); // Full control MockInterceptor mock = new MockInterceptor(validator) .withConfig(MockConfig.builder().cache(true).build()); List interactions = mock.getInteractions(); mock.clearCache(); ``` ### Java Producer Validation (Server-Side) ```java import io.github.sahina.sdk.producer.*; import io.github.sahina.sdk.producer.adapters.*; ``` **Validation Modes:** - `ValidationMode.STRICT` — Reject invalid requests with 400/422 errors - `ValidationMode.WARN` — Log violations but allow requests to proceed - `ValidationMode.SHADOW` — Validate asynchronously, record metrics only #### Spring Interceptor Requires Spring 6+ / Jakarta Servlet API. Use with `ContentCachingRequestWrapper` and `ContentCachingResponseWrapper`. ```java import io.github.sahina.sdk.producer.adapters.SpringInterceptor; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { ProducerConfig config = ProducerConfig.builder() .schemaId("user-api") .validator(myValidator) .mode(ValidationMode.STRICT) .validateRequest(true) .validateResponse(true) .includePath("/api/.*") .excludePath("/api/health") .build(); registry.addInterceptor(new SpringInterceptor(config)) .addPathPatterns("/api/**"); } } ``` #### Servlet Filter Works with any Jakarta Servlet container (Tomcat 10+, Jetty 11+). ```java import io.github.sahina.sdk.producer.adapters.ServletFilter; ProducerConfig config = ProducerConfig.builder() .schemaId("user-api") .validator(myValidator) .mode(ValidationMode.WARN) .build(); // Register as a filter FilterRegistrationBean filterRegistration = new FilterRegistrationBean<>(); filterRegistration.setFilter(new ServletFilter(config)); filterRegistration.addUrlPatterns("/api/*"); ``` #### ProducerTestKit ```java import io.github.sahina.sdk.producer.*; ProducerTestKit testKit = ProducerTestKit.builder() .schemaId("user-api") .serverAddress("localhost:9550") .build(); // Validate a response TestValidationResult result = testKit.validateResponse("GET", "/users/123", TestResponseData.builder() .statusCode(200) .body(Map.of("id", "123", "name", "John")) .build()); assertTrue(result.isValid()); // Validate full interaction TestValidationResult result = testKit.validateInteraction( TestRequestContext.builder().method("POST").path("/users").body(Map.of("name", "Jane")).build(), TestResponseData.builder().statusCode(201).body(Map.of("id", "2", "name", "Jane")).build() ); // Reusable endpoint tester ProducerTestKit.EndpointTester userEndpoint = testKit.forEndpoint("GET", "/users/{id}"); TestValidationResult r = userEndpoint.validateResponse( TestResponseData.builder().statusCode(200).body(Map.of("id", "1")).build(), Map.of("id", "1") ); testKit.close(); ``` #### Metrics ```java Producer.Metrics metrics = Producer.getMetrics(); System.out.println("Request validations: " + metrics.getRequestValidations()); System.out.println("Failed: " + metrics.getRequestValidationsFailed()); Producer.resetMetrics(); ``` ## CLI Reference ### validate Validate HTTP interaction against OpenAPI schema. ```bash # From separate request/response files cvt validate --schema ./openapi.json --request req.json --response resp.json # From combined interaction file cvt validate --schema ./openapi.json --interaction interaction.json # Inline with flags cvt validate --schema ./openapi.json \ --method GET --path /users/123 \ --status-code 200 --response-body '{"id":"123","name":"John"}' # Inline with request body cvt validate --schema ./openapi.json \ --method POST --path /users \ --request-body '{"name":"John"}' \ --status-code 201 --response-body '{"id":"1","name":"John"}' # JSON output cvt validate --schema ./openapi.json --interaction interaction.json --json # Schema from URL cvt validate --schema https://petstore3.swagger.io/api/v3/openapi.json --interaction interaction.json ``` Flags: - `--schema, -s` (required) — Path or URL to OpenAPI schema - `--interaction, -i` — Path to interaction JSON file - `--request` — Path to request JSON file - `--response` — Path to response JSON file - `--method, -m` — HTTP method (GET, POST, etc.) - `--path, -p` — Request path (e.g., /users/123) - `--request-body` — Request body as JSON string - `--status-code` — Response status code (default: 200) - `--response-body` — Response body as JSON string - `--json` — Output result as JSON ### compare Compare schemas for breaking changes. ```bash cvt compare --old ./v1/openapi.json --new ./v2/openapi.json cvt compare --old ./v1/openapi.json --new ./v2/openapi.json --json cvt compare --old ./local.json --new https://api.example.com/openapi.json ``` Flags: - `--old` (required) — Path or URL to old schema - `--new` (required) — Path or URL to new schema - `--json` — Output result as JSON Breaking change types detected: - ENDPOINT_REMOVED: Endpoint path+method was removed - REQUIRED_FIELD_ADDED: Required field added to request body - TYPE_CHANGED: Field type changed incompatibly - REQUIRED_PARAMETER_ADDED: Required query/path/header param added - RESPONSE_SCHEMA_CHANGED: Response schema changed incompatibly - ENUM_VALUE_REMOVED: Enum value was removed ### generate Generate test fixtures from schema. ```bash # List all endpoints cvt generate --schema ./openapi.json --list # Generate fixture (request + response) cvt generate --schema ./openapi.json --method POST --path /users # Generate response only cvt generate --schema ./openapi.json --method GET --path /users/{id} --output-type response # Generate request only cvt generate --schema ./openapi.json --method POST --path /users --output-type request # Use schema examples cvt generate --schema ./openapi.json --method GET --path /users/{id} --use-examples # Save to file cvt generate --schema ./openapi.json --method POST --path /users -o fixture.json # Specify status code and content type cvt generate --schema ./openapi.json --method GET --path /users/{id} --status-code 404 cvt generate --schema ./openapi.json --method POST --path /users --content-type application/json ``` Flags: - `--schema, -s` (required) — Path or URL to OpenAPI schema - `--method, -m` — HTTP method - `--path, -p` — API path (e.g., /users/{id}) - `--status-code` — Response status code (default: first successful) - `--use-examples` — Use schema examples when available (default: true) - `--content-type` — Content type (default: application/json) - `--output, -o` — Output file path (default: stdout) - `--output-type, -t` — Output type: fixture, request, response (default: fixture) - `--list, -l` — List available endpoints and exit ### serve Start gRPC server. ```bash cvt serve --port 9550 cvt serve --port 9550 --metrics-port 9551 cvt serve --port 9550 --tls --cert server.crt --key server.key cvt serve --port 9550 --api-key-auth ``` Flags: - `--port, -p` — gRPC server port (default: 9550) - `--metrics-port` — Metrics server port (default: 9551) - `--tls` — Enable TLS - `--cert` — TLS certificate file - `--key` — TLS private key file - `--api-key-auth` — Enable API key authentication ### can-i-deploy Check deployment safety. ```bash cvt can-i-deploy --schema my-api --version 2.0.0 --env prod cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --server localhost:9550 cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --json cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --timeout 60 ``` Flags: - `--schema` (required) — Schema ID to check - `--version` (required) — New version to deploy - `--env` (required) — Target environment: dev, staging, prod - `--server` — CVT server address (default: localhost:9550) - `--json` — Output result as JSON - `--timeout` — Connection timeout in seconds (default: 30) ### wait Wait for CVT server to be ready. ```bash cvt wait # Default: 60s timeout, 2s interval cvt wait --server localhost:9550 # Specific server address cvt wait --timeout 120 --interval 1 # Custom timeout and polling interval cvt wait -S localhost:9550 -t 120 -i 1 # Short flags cvt wait -q # Quiet mode for CI/CD cvt wait --json # JSON output for scripting ``` Flags: - `--server, -S` — CVT server address (default: localhost:9550) - `--timeout, -t` — Maximum time to wait in seconds (default: 60) - `--interval, -i` — Polling interval in seconds (default: 2) - `--quiet, -q` — Suppress output except errors - `--json, -j` — Output result as JSON JSON output: ```json { "status": "ready", "server": "localhost:9550", "attempts": 3 } ``` ### register-schema Register an OpenAPI schema with the CVT server. ```bash cvt register-schema my-api ./openapi.yaml # Basic registration cvt register-schema my-api https://api.example.com/openapi.json # Register from URL cvt register-schema my-api ./openapi.yaml --version 2.0.0 # With specific version cvt register-schema my-api ./openapi.yaml -v 2.0.0 # Short flag cvt register-schema my-api ./openapi.yaml --check-compatibility # Check for breaking changes cvt register-schema my-api ./openapi.yaml --check-compatibility --fail-on-breaking # Fail CI on breaking cvt register-schema my-api ./openapi.yaml --json # JSON output for scripting cvt register-schema my-api ./openapi.yaml --owner "Jane Doe" --team "Platform" # With ownership cvt register-schema my-api ./openapi.yaml -q # Quiet mode ``` Positional arguments: - `` (required) — Unique identifier for the schema - `` (required) — Path or URL to OpenAPI schema file Flags: - `--server, -S` — CVT server address (default: localhost:9550) - `--version, -v` — Schema version (optional, extracted from spec if not provided) - `--check-compatibility` — Check for breaking changes against previous version - `--fail-on-breaking` — Exit with error if breaking changes detected (use with --check-compatibility) - `--json, -j` — Output result as JSON - `--quiet, -q` — Suppress output except errors - `--timeout, -t` — Connection timeout in seconds (default: 30) - `--owner` — Schema owner name - `--team` — Owning team name JSON output: ```json { "success": true, "message": "Schema registered successfully", "schema_id": "my-api", "version": "2.0.0", "breaking_changes": [] } ``` ### version Show version information. ```bash cvt version ``` ## gRPC API ### Service: ContractValidator ```protobuf service ContractValidator { // Schema Management rpc RegisterSchema(RegisterSchemaRequest) returns (RegisterSchemaResponse); rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse); rpc ListSchemas(ListSchemasRequest) returns (ListSchemasResponse); // Validation rpc ValidateInteraction(InteractionRequest) returns (ValidationResult); rpc ValidateProducerResponse(ValidateProducerRequest) returns (ValidationResult); // Schema Comparison rpc CompareSchemas(CompareSchemasRequest) returns (CompareSchemasResponse); // Fixtures rpc GenerateFixture(GenerateFixtureRequest) returns (GenerateFixtureResponse); rpc ListEndpoints(ListEndpointsRequest) returns (ListEndpointsResponse); // Consumer Registry rpc RegisterConsumer(RegisterConsumerRequest) returns (RegisterConsumerResponse); rpc ListConsumers(ListConsumersRequest) returns (ListConsumersResponse); rpc DeregisterConsumer(DeregisterConsumerRequest) returns (DeregisterConsumerResponse); // Deployment Safety rpc CanIDeploy(CanIDeployRequest) returns (CanIDeployResponse); } ``` ### Message Types ```protobuf // ===== Schema Management ===== message SchemaOwnership { string owner = 1; // Owner name or identifier string team = 2; // Team responsible for the schema string contact_email = 3; // Contact email for schema issues bool read_only = 4; // If true, schema cannot be updated } message SchemaMetadata { string schema_id = 1; // Unique identifier string schema_version = 2; // Semantic version (e.g., "1.2.3") string schema_hash = 3; // SHA256 hash of schema content int64 registered_at = 4; // Unix timestamp of initial registration int64 updated_at = 5; // Unix timestamp of last update SchemaOwnership ownership = 6; string openapi_version = 7; // Detected OpenAPI version (e.g., "3.0.0") int32 endpoint_count = 8; // Number of endpoints in schema } message RegisterSchemaRequest { string schema_id = 1; string schema_content = 2; // OpenAPI YAML or JSON string schema_version = 3; // Semantic version SchemaOwnership ownership = 4; bool check_compatibility = 5; } message RegisterSchemaResponse { bool success = 1; string message = 2; SchemaMetadata metadata = 3; // Metadata for the registered schema repeated BreakingChange breaking_changes = 4; // Breaking changes if check_compatibility was true } message GetSchemaRequest { string schema_id = 1; string schema_version = 2; // Optional: specific version (empty = latest) } message GetSchemaResponse { bool found = 1; SchemaMetadata metadata = 2; string schema_content = 3; // The actual schema content } message ListSchemasRequest { int32 page_size = 1; // Max results per page (default 100) string page_token = 2; // Token for pagination string owner = 3; // Optional: filter by owner string team = 4; // Optional: filter by team } message ListSchemasResponse { repeated SchemaMetadata schemas = 1; string next_page_token = 2; // Token for next page (empty if no more) int32 total_count = 3; // Total number of schemas matching filter } // ===== Validation ===== message InteractionRequest { string schema_id = 1; RequestData request = 2; ResponseData response = 3; string schema_version = 4; // Optional: validate against specific version } message RequestData { string method = 1; // GET, POST, etc. string path = 2; // /users/123 map headers = 3; string body = 4; // JSON body as string } message ResponseData { int32 status_code = 1; map headers = 2; string body = 3; // JSON body as string } message ValidationResult { bool valid = 1; repeated string errors = 2; string validated_against_version = 3; string validated_against_hash = 4; } message ValidateProducerRequest { string schema_id = 1; // Schema to validate against string schema_version = 2; // Optional: specific schema version string method = 3; // HTTP method (GET, POST, etc.) string path = 4; // API path with actual values (e.g., /users/123) ResponseData response = 5; // The response to validate RequestData request = 6; // Optional: request context (for path param extraction) } // ===== Schema Comparison ===== message CompareSchemasRequest { string schema_id = 1; string old_version = 2; // Version to compare from (empty = previous) string new_version = 3; // Version to compare to (empty = latest) } message CompareSchemasResponse { bool compatible = 1; // True if no breaking changes repeated BreakingChange breaking_changes = 2; SchemaMetadata old_schema = 3; SchemaMetadata new_schema = 4; } message BreakingChange { BreakingChangeType type = 1; string path = 2; // API path affected (e.g., "/users/{id}") string method = 3; // HTTP method affected (e.g., "POST") string description = 4; // Human-readable description string old_value = 5; // Previous value (for context) string new_value = 6; // New value (for context) } enum BreakingChangeType { BREAKING_CHANGE_UNSPECIFIED = 0; ENDPOINT_REMOVED = 1; // Endpoint path+method was removed REQUIRED_FIELD_ADDED = 2; // Required field added to request body TYPE_CHANGED = 3; // Field type changed incompatibly REQUIRED_PARAMETER_ADDED = 4; // Required query/path/header param added RESPONSE_SCHEMA_CHANGED = 5; // Response schema changed incompatibly ENUM_VALUE_REMOVED = 6; // Enum value was removed } // ===== Fixtures ===== message GenerateFixtureRequest { string schema_id = 1; // Schema to generate fixtures from string method = 2; // HTTP method (GET, POST, etc.) string path = 3; // API path (e.g., /users/{id}) int32 status_code = 4; // Response status code (0 = auto-select) bool use_examples = 5; // Use schema examples when available string content_type = 6; // Content type (default: application/json) OutputType output_type = 7; // What to generate } enum OutputType { OUTPUT_FIXTURE = 0; // Complete request/response pair (default) OUTPUT_REQUEST = 1; // Request body only OUTPUT_RESPONSE = 2; // Response only } message GenerateFixtureResponse { bool success = 1; string message = 2; GeneratedFixture fixture = 3; // Full fixture (if output_type = FIXTURE) string request_body = 4; // Request body JSON (if output_type = REQUEST) GeneratedResponse response = 5; // Response (if output_type = RESPONSE) } message GeneratedFixture { GeneratedRequest request = 1; GeneratedResponse response = 2; } message GeneratedRequest { string method = 1; string path = 2; map headers = 3; string body = 4; // JSON body as string } message GeneratedResponse { int32 status_code = 1; map headers = 2; string body = 3; // JSON body as string } message ListEndpointsRequest { string schema_id = 1; } message ListEndpointsResponse { repeated EndpointInfo endpoints = 1; } message EndpointInfo { string method = 1; // HTTP method string path = 2; // API path string operation_id = 3; // OpenAPI operationId (if defined) string summary = 4; // OpenAPI summary (if defined) } // ===== Consumer Registry ===== message ConsumerInfo { string consumer_id = 1; string consumer_version = 2; string schema_id = 3; string schema_version = 4; string environment = 5; // dev, staging, prod int64 registered_at = 6; // Unix timestamp int64 last_validated_at = 7; // Unix timestamp repeated EndpointUsage used_endpoints = 8; } message EndpointUsage { string method = 1; string path = 2; repeated string used_fields = 3; // Fields used in response (e.g., ["email", "name"]) } message RegisterConsumerRequest { string consumer_id = 1; string consumer_version = 2; string schema_id = 3; string schema_version = 4; string environment = 5; // dev, staging, prod repeated EndpointUsage used_endpoints = 6; } message RegisterConsumerResponse { bool success = 1; string message = 2; ConsumerInfo consumer = 3; } message ListConsumersRequest { string schema_id = 1; // Required: schema to query string environment = 2; // Optional: filter by environment } message ListConsumersResponse { repeated ConsumerInfo consumers = 1; } message DeregisterConsumerRequest { string consumer_id = 1; string schema_id = 2; string environment = 3; } message DeregisterConsumerResponse { bool success = 1; string message = 2; } // ===== Deployment Safety ===== message CanIDeployRequest { string schema_id = 1; string new_version = 2; string environment = 3; } message CanIDeployResponse { bool safe_to_deploy = 1; string summary = 2; repeated BreakingChange breaking_changes = 3; repeated ConsumerImpact affected_consumers = 4; } message ConsumerImpact { string consumer_id = 1; string consumer_version = 2; string current_schema_version = 3; // Version consumer was tested against string environment = 4; bool will_break = 5; // True if consumer will be affected repeated BreakingChange relevant_changes = 6; // Breaking changes affecting this consumer } ``` ## Configuration ### Environment Variables | Variable | Default | Description | |----------|---------|-------------| | CVT_PORT | 9550 | gRPC server port | | CVT_METRICS_PORT | 9551 | Prometheus metrics port | | LOG_LEVEL | info | Logging level (debug, info, warn, error) | #### TLS | Variable | Default | Description | |----------|---------|-------------| | CVT_TLS_ENABLED | false | Enable TLS | | CVT_TLS_CERT_FILE | /certs/server.crt | Server certificate | | CVT_TLS_KEY_FILE | /certs/server.key | Server private key | | CVT_TLS_CA_FILE | | CA certificate for mTLS | | CVT_TLS_CLIENT_AUTH | none | Client auth: none, request, require | #### API Key Authentication | Variable | Default | Description | |----------|---------|-------------| | CVT_API_KEY_ENABLED | false | Enable API key auth | | CVT_API_KEYS | | Comma-separated API keys | | CVT_API_KEYS_FILE | | Path to JSON config | #### Storage | Variable | Default | Description | |----------|---------|-------------| | CVT_STORAGE_ENABLED | false | Enable persistent storage | | CVT_STORAGE_TYPE | sqlite | Storage backend: sqlite, postgres, memory | | CVT_STORAGE_DSN | cvt.db | SQLite database path | | CVT_POSTGRES_HOST | localhost | PostgreSQL host | | CVT_POSTGRES_PORT | 5432 | PostgreSQL port | | CVT_POSTGRES_USER | cvt | PostgreSQL user | | CVT_POSTGRES_PASSWORD | | PostgreSQL password | | CVT_POSTGRES_DB | cvt | PostgreSQL database | | CVT_POSTGRES_SSLMODE | disable | PostgreSQL SSL mode | | CVT_POSTGRES_DSN | | Full PostgreSQL DSN (alternative to individual host/port/user/password/db) | | CVT_POSTGRES_MAX_CONNS | 25 | Maximum open PostgreSQL connections | | CVT_STORAGE_CACHE_ENABLED | true | Enable in-memory cache | | CVT_STORAGE_CACHE_MAX_SCHEMAS | 1000 | Max schemas in cache | | CVT_VALIDATION_RETENTION_DAYS | 90 | Days to retain validation records | ### API Keys JSON Format ```json { "keys": [ { "key": "cvt-dev-key-12345", "name": "development", "scopes": ["*"] }, { "key": "cvt-prod-key-abcde", "name": "production", "scopes": ["*"], "expires_at": "2026-01-01T00:00:00Z" } ] } ``` ## Common Workflows ### Consumer Contract Testing #### Node.js ```typescript await validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0"); const result = await validator.validate( { method: "GET", path: "/users/123", headers: {} }, { statusCode: 200, body: { id: "123", name: "John", email: "john@example.com" } } ); assert(result.valid); await validator.registerConsumer({ consumerId: "order-service", consumerVersion: "2.1.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev", usedEndpoints: [ { method: "GET", path: "/users/{id}", usedFields: ["id", "name", "email"] } ] }); ``` #### Python ```python validator.register_schema_with_version("user-api", "./openapi.json", "1.0.0") result = validator.validate( request={"method": "GET", "path": "/users/123", "headers": {}}, response={"status_code": 200, "body": {"id": "123", "name": "John", "email": "john@example.com"}} ) assert result["valid"] validator.register_consumer(RegisterConsumerOptions( consumer_id="order-service", consumer_version="2.1.0", schema_id="user-api", schema_version="1.0.0", environment="dev", used_endpoints=[ EndpointUsage(method="GET", path="/users/{id}", used_fields=["id", "name", "email"]) ] )) ``` #### Go ```go ctx := context.Background() validator.RegisterSchemaWithVersion(ctx, "user-api", "./openapi.json", "1.0.0") result, _ := validator.Validate(ctx, cvt.ValidationRequest{Method: "GET", Path: "/users/123"}, cvt.ValidationResponse{StatusCode: 200, Body: map[string]any{ "id": "123", "name": "John", "email": "john@example.com", }}, ) assert.True(t, result.Valid) validator.RegisterConsumer(ctx, cvt.RegisterConsumerOptions{ ConsumerID: "order-service", ConsumerVersion: "2.1.0", SchemaID: "user-api", SchemaVersion: "1.0.0", Environment: "dev", UsedEndpoints: []cvt.EndpointUsage{ {Method: "GET", Path: "/users/{id}", UsedFields: []string{"id", "name", "email"}}, }, }) ``` #### Java ```java validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0"); ValidationResult result = validator.validate( ValidationRequest.builder().method("GET").path("/users/123").build(), ValidationResponse.builder().statusCode(200) .body("{\"id\":\"123\",\"name\":\"John\",\"email\":\"john@example.com\"}").build() ); assertTrue(result.isValid()); validator.registerConsumer(RegisterConsumerOptions.builder() .consumerId("order-service").consumerVersion("2.1.0") .schemaId("user-api").schemaVersion("1.0.0").environment("dev") .usedEndpoints(List.of( new EndpointUsage("GET", "/users/{id}", List.of("id", "name", "email")) )).build()); ``` ### Deployment Safety Check #### Node.js ```typescript const result = await validator.canIDeploy("user-api", "2.0.0", "prod"); if (!result.safeToDeploy) { console.error("UNSAFE TO DEPLOY:", result.summary); for (const consumer of result.affectedConsumers) { if (consumer.willBreak) { console.error(`${consumer.consumerId} v${consumer.consumerVersion} will break:`); for (const change of consumer.relevantChanges) { console.error(` - ${change.type}: ${change.description}`); } } } process.exit(1); } ``` #### Python ```python result = validator.can_i_deploy("user-api", "2.0.0", "prod") if not result["safe_to_deploy"]: print("UNSAFE TO DEPLOY:", result["summary"]) for consumer in result["affected_consumers"]: if consumer["will_break"]: print(f" {consumer['consumer_id']} v{consumer['consumer_version']} will break:") for change in consumer["relevant_changes"]: print(f" - {change['type']}: {change['description']}") sys.exit(1) ``` #### Go ```go result, _ := validator.CanIDeploy(ctx, "user-api", "2.0.0", "prod") if !result.SafeToDeploy { fmt.Println("UNSAFE TO DEPLOY:", result.Summary) for _, c := range result.AffectedConsumers { if c.WillBreak { fmt.Printf(" %s v%s will break:\n", c.ConsumerID, c.ConsumerVersion) for _, change := range c.RelevantChanges { fmt.Printf(" - %s: %s\n", change.Type, change.Description) } } } os.Exit(1) } ``` #### Java ```java CanIDeployResult result = validator.canIDeploy("user-api", "2.0.0", "prod"); if (!result.isSafeToDeploy()) { System.err.println("UNSAFE TO DEPLOY: " + result.getSummary()); for (ConsumerImpact c : result.getAffectedConsumers()) { if (c.isWillBreak()) { System.err.println(" " + c.getConsumerId() + " v" + c.getConsumerVersion() + " will break:"); for (BreakingChange change : c.getRelevantChanges()) { System.err.println(" - " + change.getType() + ": " + change.getDescription()); } } } System.exit(1); } ``` ### Producer Testing Workflow Validate that your API server's responses conform to the schema. #### Express + Strict Mode ```typescript import express from "express"; import { ContractValidator } from "@sahina/cvt-sdk"; import { createExpressMiddleware } from "@sahina/cvt-sdk/producer"; const app = express(); const validator = new ContractValidator("localhost:9550"); await validator.registerSchema("user-api", "./openapi.json"); app.use(createExpressMiddleware({ schemaId: "user-api", validator, mode: "strict", // Reject invalid requests })); app.get("/users/:id", (req, res) => { res.json({ id: req.params.id, name: "John" }); }); ``` #### FastAPI + Shadow Mode ```python from fastapi import FastAPI from cvt_sdk import ContractValidator from cvt_sdk.producer import ProducerConfig, ValidationMode from cvt_sdk.producer.adapters import ASGIMiddleware app = FastAPI() validator = ContractValidator("localhost:9550") validator.register_schema("user-api", "./openapi.json") app.add_middleware(ASGIMiddleware, config=ProducerConfig( schema_id="user-api", validator=validator, mode=ValidationMode.SHADOW, # Async validation, metrics only )) ``` #### Go (Chi) + Warn Mode ```go import ( "net/http" "github.com/go-chi/chi/v5" cvt "github.com/sahina/cvt/sdks/go/cvt" "github.com/sahina/cvt/sdks/go/cvt/producer" producerAdapters "github.com/sahina/cvt/sdks/go/cvt/producer/adapters" ) validator, _ := cvt.NewValidator("localhost:9550") validator.RegisterSchema(ctx, "user-api", "./openapi.json") r := chi.NewRouter() r.Use(producerAdapters.ChiMiddleware(producer.Config{ SchemaID: "user-api", Validator: validator, Mode: producer.ModeWarn, // Log violations, allow requests })) r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]any{"id": chi.URLParam(r, "id"), "name": "John"}) }) ``` #### Spring + Warn Mode ```java @Configuration public class CvtConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { ContractValidator validator = new ContractValidator("localhost:9550"); validator.registerSchema("user-api", "./openapi.json"); registry.addInterceptor(new SpringInterceptor(ProducerConfig.builder() .schemaId("user-api") .validator(myValidator) .mode(ValidationMode.WARN) // Log violations, allow requests .build())) .addPathPatterns("/api/**"); } } ``` ### HTTP Client Adapter Workflow Transparently validate HTTP interactions and capture them for consumer registration. #### Node.js with Axios ```typescript import axios from "axios"; import { ContractValidator } from "@sahina/cvt-sdk"; import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters"; const validator = new ContractValidator("localhost:9550"); await validator.registerSchema("user-api", "./openapi.json"); const api = axios.create({ baseURL: "http://localhost:3000" }); const adapter = createAxiosAdapter({ validator, axios: api }); // Run your tests — all HTTP calls are validated automatically const users = await api.get("/users"); const user = await api.post("/users", { name: "Jane" }); // Auto-register as consumer from captured interactions await validator.registerConsumerFromInteractions( adapter.getInteractions(), { consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev" } ); ``` #### Python with Requests ```python from cvt_sdk import ContractValidator from cvt_sdk.adapters import create_validating_session validator = ContractValidator("localhost:9550") validator.register_schema("user-api", "./openapi.json") session = create_validating_session(validator) # Run your tests — all HTTP calls are validated automatically response = session.get("http://localhost:3000/users/123") response = session.post("http://localhost:3000/users", json={"name": "Jane"}) # Auto-register as consumer validator.register_consumer_from_interactions( interactions=session.get_interactions(), options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "schema_version": "1.0.0", "environment": "dev"} ) ``` #### Go with http.Client ```go validator, _ := cvt.NewValidator("localhost:9550") validator.RegisterSchema(ctx, "user-api", "./openapi.json") rt := adapters.NewValidatingRoundTripper(adapters.RoundTripperConfig{ Validator: validator, AutoValidate: true, }) client := &http.Client{Transport: rt} // Run your tests — all HTTP calls are validated resp, _ := client.Get("http://localhost:3000/users/123") // Auto-register as consumer validator.RegisterConsumerFromInteractions(ctx, rt.GetInteractions(), cvt.AutoRegisterConfig{ ConsumerID: "order-service", ConsumerVersion: "1.0.0", Environment: "dev", SchemaVersion: "1.0.0", SchemaID: "user-api", }) ``` #### Java with OkHttp ```java ContractValidator validator = new ContractValidator("localhost:9550"); validator.registerSchema("user-api", "./openapi.json"); OkHttpContractAdapter adapter = new OkHttpContractAdapter(validator); OkHttpClient client = adapter.attach(new OkHttpClient.Builder()).build(); // Run your tests — all HTTP calls are validated Response resp = client.newCall(new Request.Builder() .url("http://localhost:3000/users/123").build()).execute(); // Auto-register as consumer validator.registerConsumerFromInteractions(adapter.getInteractions(), AutoRegisterConfig.builder() .consumerId("order-service").consumerVersion("1.0.0") .schemaId("user-api").schemaVersion("1.0.0").environment("dev") .build()); ``` ### Auto-Registration Workflow Build consumer registrations automatically from recorded HTTP interactions. #### Node.js ```typescript import { ContractValidator } from "@sahina/cvt-sdk"; import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters"; const adapter = createAxiosAdapter({ validator, axios: api }); // Run integration tests — interactions are captured automatically await api.get("/users/123"); await api.post("/users", { name: "Jane" }); // Dry run: inspect what would be registered const consumerData = validator.buildConsumerFromInteractions( adapter.getInteractions(), { consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", environment: "dev" } ); console.log("Endpoints used:", consumerData.usedEndpoints); // Register consumer from captured interactions await validator.registerConsumerFromInteractions( adapter.getInteractions(), { consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev" } ); ``` #### Python ```python from cvt_sdk import ContractValidator from cvt_sdk.adapters import create_validating_session session = create_validating_session(validator) # Run integration tests — interactions are captured automatically session.get("http://localhost:3000/users/123") session.post("http://localhost:3000/users", json={"name": "Jane"}) # Dry run: inspect what would be registered consumer_data, err = validator.build_consumer_from_interactions( interactions=session.get_interactions(), options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "environment": "dev"} ) # Register consumer from captured interactions validator.register_consumer_from_interactions( interactions=session.get_interactions(), options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "schema_version": "1.0.0", "environment": "dev"} ) ``` #### Go ```go rt := adapters.NewValidatingRoundTripper(adapters.RoundTripperConfig{ Validator: validator, AutoValidate: true, }) client := &http.Client{Transport: rt} // Run integration tests — interactions are captured automatically client.Get("http://localhost:3000/users/123") // Dry run: inspect what would be registered opts, _ := validator.BuildConsumerFromInteractions(ctx, rt.GetInteractions(), cvt.AutoRegisterConfig{ ConsumerID: "order-service", ConsumerVersion: "1.0.0", SchemaID: "user-api", Environment: "dev", }) fmt.Println("Endpoints used:", opts.UsedEndpoints) // Register consumer from captured interactions validator.RegisterConsumerFromInteractions(ctx, rt.GetInteractions(), cvt.AutoRegisterConfig{ ConsumerID: "order-service", ConsumerVersion: "1.0.0", SchemaID: "user-api", SchemaVersion: "1.0.0", Environment: "dev", }) ``` #### Java ```java OkHttpContractAdapter adapter = new OkHttpContractAdapter(validator); OkHttpClient client = adapter.attach(new OkHttpClient.Builder()).build(); // Run integration tests — interactions are captured automatically client.newCall(new Request.Builder().url("http://localhost:3000/users/123").build()).execute(); // Dry run: inspect what would be registered AutoRegisterUtils.BuildResult consumerData = validator.buildConsumerFromInteractions( adapter.getInteractions(), AutoRegisterConfig.builder() .consumerId("order-service").consumerVersion("1.0.0") .schemaId("user-api").environment("dev").build()); // Register consumer from captured interactions validator.registerConsumerFromInteractions(adapter.getInteractions(), AutoRegisterConfig.builder() .consumerId("order-service").consumerVersion("1.0.0") .schemaId("user-api").schemaVersion("1.0.0").environment("dev").build()); ``` ### CI/CD Integration #### GitHub Actions ```yaml name: Contract Validation on: [push, pull_request] jobs: validate: runs-on: ubuntu-latest services: cvt: image: ghcr.io/sahina/cvt:latest ports: - 9550:9550 steps: - uses: actions/checkout@v4 - name: Wait for CVT server run: cvt wait --server localhost:9550 --timeout 60 - name: Register schema run: cvt register-schema my-api ./openapi.yaml --version ${{ github.sha }} - name: Check for breaking changes run: | cvt register-schema my-api ./openapi.yaml \ --check-compatibility --fail-on-breaking --json - name: Validate test fixtures run: | for fixture in tests/fixtures/*.json; do cvt validate --schema ./openapi.yaml --interaction "$fixture" done - name: Run SDK tests run: npm test - name: Check deployment safety if: github.ref == 'refs/heads/main' run: | cvt can-i-deploy --schema my-api \ --version ${{ github.sha }} --env staging --json ``` #### Shell Script for CI ```bash #!/bin/bash set -euo pipefail CVT_SERVER="${CVT_SERVER:-localhost:9550}" SCHEMA_ID="${SCHEMA_ID:-my-api}" VERSION="${VERSION:-$(git rev-parse --short HEAD)}" # Wait for server cvt wait --server "$CVT_SERVER" --timeout 120 -q # Register schema with breaking change check cvt register-schema "$SCHEMA_ID" ./openapi.yaml \ --version "$VERSION" \ --server "$CVT_SERVER" \ --check-compatibility \ --fail-on-breaking # Validate all fixture files for fixture in tests/fixtures/*.json; do echo "Validating $fixture..." cvt validate --schema ./openapi.yaml --interaction "$fixture" done # Check deployment safety cvt can-i-deploy \ --schema "$SCHEMA_ID" \ --version "$VERSION" \ --env "${TARGET_ENV:-staging}" \ --server "$CVT_SERVER" echo "All checks passed!" ``` ### Test Fixture Generation #### Node.js ```typescript const endpoints = await validator.listEndpoints(); for (const ep of endpoints) { const fixture = await validator.generateFixture(ep.method, ep.path, { useExamples: true }); console.log(`${ep.method} ${ep.path} request:`, fixture.request.body); console.log(`${ep.method} ${ep.path} response:`, fixture.response.body); } // Generate just a response const response = await validator.generateResponse("GET", "/users/123"); // Generate just a request body const body = await validator.generateRequestBody("POST", "/users"); ``` #### Python ```python endpoints = validator.list_endpoints() for ep in endpoints: fixture = validator.generate_fixture(ep["method"], ep["path"], GenerateOptions(use_examples=True)) print(f"{ep['method']} {ep['path']} request: {fixture['request']['body']}") print(f"{ep['method']} {ep['path']} response: {fixture['response']['body']}") # Generate just a response response = validator.generate_response("GET", "/users/123") # Generate just a request body body = validator.generate_request_body("POST", "/users") ``` #### Go ```go endpoints, _ := validator.ListEndpoints(ctx) for _, ep := range endpoints { fixture, _ := validator.GenerateFixture(ctx, ep.Method, ep.Path, &cvt.GenerateOptions{UseExamples: true}) fmt.Printf("%s %s request: %v\n", ep.Method, ep.Path, fixture.Request.Body) fmt.Printf("%s %s response: %v\n", ep.Method, ep.Path, fixture.Response.Body) } // Generate just a response response, _ := validator.GenerateResponse(ctx, "GET", "/users/123", nil) // Generate just a request body body, _ := validator.GenerateRequestBody(ctx, "POST", "/users", nil) ``` #### Java ```java List endpoints = validator.listEndpoints(); for (EndpointInfo ep : endpoints) { GeneratedFixture fixture = validator.generateFixture(ep.getMethod(), ep.getPath(), GenerateOptions.builder().useExamples(true).build()); System.out.println(ep.getMethod() + " " + ep.getPath() + " request: " + fixture.getRequest().getBody()); System.out.println(ep.getMethod() + " " + ep.getPath() + " response: " + fixture.getResponse().getBody()); } // Generate just a response GeneratedResponse response = validator.generateResponse("GET", "/users/123", null); // Generate just a request body Object body = validator.generateRequestBody("POST", "/users", null); ``` ### Mock Testing Workflow Test consumers against schema-generated mock responses — no real API needed. #### Node.js with Mock Fetch ```typescript import { ContractValidator } from "@sahina/cvt-sdk"; import { createMockFetch } from "@sahina/cvt-sdk/adapters"; const validator = new ContractValidator("localhost:9550"); await validator.registerSchema("user-api", "./openapi.json"); const mockFetch = createMockFetch(validator, { cache: true }); // Tests use mock responses generated from the schema const response = await mockFetch("/users/123"); const user = await response.json(); expect(user).toHaveProperty("id"); expect(user).toHaveProperty("name"); ``` #### Python with Mock Session ```python from cvt_sdk import ContractValidator from cvt_sdk.adapters import create_mock_session validator = ContractValidator("localhost:9550") validator.register_schema("user-api", "./openapi.json") mock = create_mock_session(validator, cache=True) response = mock.get("/users/123") user = response.json() assert "id" in user assert "name" in user ``` #### Go with Mock Client ```go mockClient := adapters.NewMockClient(validator, adapters.WithCache()) resp, _ := mockClient.Get("http://api.example.com/users/123") defer resp.Body.Close() // Response body is generated from the OpenAPI schema ``` #### Java with Mock OkHttp ```java OkHttpClient mockClient = MockInterceptor.createMockClient(validator, MockConfig.builder().cache(true).build()); Response response = mockClient.newCall(new Request.Builder() .url("http://api.example.com/users/123").build()).execute(); // Response body is generated from the OpenAPI schema ``` ## Deployment ### Docker ```bash # Pull and run docker pull ghcr.io/sahina/cvt:latest docker run -p 9550:9550 -p 9551:9551 ghcr.io/sahina/cvt:latest # With environment variables docker run -p 9550:9550 \ -e CVT_STORAGE_ENABLED=true \ -e CVT_STORAGE_TYPE=sqlite \ -e LOG_LEVEL=debug \ ghcr.io/sahina/cvt:latest ``` ### Docker Compose (Full Stack) ```bash # Start server + PostgreSQL + Prometheus + Grafana docker compose up -d # Check health cvt wait --server localhost:9550 ``` ## Ports | Port | Service | |------|---------| | 9550 | gRPC server | | 9551 | Prometheus metrics | | 9091 | Prometheus UI (Docker) | | 3000 | Grafana UI (Docker) | | 5432 | PostgreSQL (Docker) | ## Package Registries | SDK | Package | Registry | |-----|---------|----------| | Node.js | `@sahina/cvt-sdk` | [npm](https://www.npmjs.com/package/@sahina/cvt-sdk) | | Python | `cvt-sdk` | [PyPI](https://pypi.org/project/cvt-sdk/) | | Go | `github.com/sahina/cvt/sdks/go` | [pkg.go.dev](https://pkg.go.dev/github.com/sahina/cvt/sdks/go) | | Java | `io.github.sahina:cvt-sdk` | [Maven Central](https://central.sonatype.com/artifact/io.github.sahina/cvt-sdk) | | Docker | `ghcr.io/sahina/cvt` | [GHCR](https://ghcr.io/sahina/cvt) |