armature-framework 0.2.2

A modern, type-safe HTTP framework for Rust inspired by Angular and NestJS. Features dependency injection, decorators, middleware, authentication (JWT/OAuth2/SAML), validation, OpenAPI/Swagger, caching, job queues, and observability.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# API Versioning Guide

Armature provides comprehensive API versioning support with multiple strategies for version extraction and flexible version management.

## Table of Contents

- [Overview]#overview
- [Features]#features
- [Versioning Strategies]#versioning-strategies
- [Basic Usage]#basic-usage
- [Version Configuration]#version-configuration
- [Versioned Handlers]#versioned-handlers
- [Request and Response Extensions]#request-and-response-extensions
- [Migration Strategies]#migration-strategies
- [Best Practices]#best-practices
- [API Reference]#api-reference
- [Summary]#summary

## Overview

API versioning allows you to evolve your API over time while maintaining backward compatibility. Armature supports multiple versioning strategies:

- **URL Path**: `/v1/users`, `/v2/users`
- **Header**: `X-API-Version: 1`
- **Query Parameter**: `/users?api-version=1`
- **Media Type**: `Accept: application/vnd.myapi.v1+json`

## Features

- ✅ Multiple versioning strategies
- ✅ Semantic versioning support (1.0, 2.1, etc.)
- ✅ Version constraints (exact, minimum, range)
- ✅ Deprecated version warnings
- ✅ Response header injection
- ✅ Combined/fallback strategies
- ✅ Version-specific route handlers

## Versioning Strategies

### URL Path Versioning

The most common and visible approach:

```rust
use armature_core::versioning::{VersioningStrategy, ApiVersion};

let strategy = VersioningStrategy::url_path();

// Matches: /v1/users, /v2/users/123, /v3/posts
```

With a prefix:

```rust
let strategy = VersioningStrategy::url_path_with_prefix("/api");

// Matches: /api/v1/users, /api/v2/users
```

### Header Versioning

Version in HTTP header (cleaner URLs):

```rust
let strategy = VersioningStrategy::header();
// Uses X-API-Version header by default

// Or custom header name
let strategy = VersioningStrategy::header_with_name("API-Version");
```

Client sends:
```http
GET /users HTTP/1.1
X-API-Version: 2
```

### Query Parameter Versioning

Version in query string:

```rust
let strategy = VersioningStrategy::query_param();
// Uses api-version parameter by default

// Or custom parameter name
let strategy = VersioningStrategy::query_param_with_name("version");
```

URL: `/users?api-version=2`

### Media Type Versioning

Version in Accept header (RESTful approach):

```rust
let strategy = VersioningStrategy::media_type("vnd.myapi");
```

Client sends:
```http
GET /users HTTP/1.1
Accept: application/vnd.myapi.v2+json
```

### Combined Strategies

Try multiple strategies in order:

```rust
// Default: URL path → Header → Query param
let strategy = VersioningStrategy::default_combined();

// Custom order
let strategy = VersioningStrategy::combined(vec![
    VersioningStrategy::header(),
    VersioningStrategy::query_param(),
    VersioningStrategy::url_path(),
]);
```

## Basic Usage

### Extracting Version from Request

```rust
use armature_core::{HttpRequest, HttpResponse, Error};
use armature_core::versioning::{VersioningStrategy, VersionedRequest, ApiVersion};

async fn handle_request(req: HttpRequest) -> Result<HttpResponse, Error> {
    let strategy = VersioningStrategy::url_path();

    // Extract version
    let version = req.api_version(&strategy)
        .unwrap_or(ApiVersion::V1);

    // Route based on version
    match version.major {
        1 => handle_v1(req).await,
        2 => handle_v2(req).await,
        _ => Err(Error::BadRequest(format!(
            "Unsupported API version: {}",
            version
        ))),
    }
}
```

### Convenience Methods

```rust
use armature_core::versioning::VersionedRequest;

// URL path version
let version = req.url_version();

// Header version
let version = req.header_version("X-API-Version");

// Query param version
let version = req.query_version("api-version");
```

### Adding Version to Response

```rust
use armature_core::versioning::{VersionedResponse, ApiVersion};

let response = HttpResponse::ok()
    .with_json(&data)?
    .with_api_version(ApiVersion::V2);
```

## Version Configuration

### Basic Configuration

```rust
use armature_core::versioning::{VersionConfig, VersioningStrategy, ApiVersion};

let config = VersionConfig::new(VersioningStrategy::url_path())
    .default_version(ApiVersion::V2)
    .supported_versions([ApiVersion::V1, ApiVersion::V2, ApiVersion::V3])
    .deprecated(ApiVersion::V1)
    .require_version(false)
    .add_response_headers(true);
```

### Resolving Version from Request

```rust
async fn handle(req: HttpRequest, config: &VersionConfig) -> Result<HttpResponse, Error> {
    // Resolve version (uses default if not provided)
    let version = config.resolve_version(&req)?;

    // Process request...
    let response = process(&req, &version)?;

    // Apply version headers
    Ok(config.apply_headers(response, &version))
}
```

### Configuration Options

| Option | Description |
|--------|-------------|
| `default_version` | Version to use when none provided |
| `supported_versions` | List of valid versions |
| `deprecated` | Mark version as deprecated |
| `require_version` | Fail if version not provided |
| `add_response_headers` | Add version info to responses |

## Versioned Handlers

Route to different handlers based on version:

```rust
use armature_core::versioning::{VersionedHandler, ApiVersion};

// Create versioned handler registry
let handler = VersionedHandler::new()
    .version(ApiVersion::V1, handle_users_v1)
    .version(ApiVersion::V2, handle_users_v2)
    .version(ApiVersion::V3, handle_users_v3)
    .fallback(handle_users_latest);

// Get handler for version
if let Some(handler) = handler.get(&version) {
    handler(req).await
}

// Or get compatible handler (same major version)
if let Some(handler) = handler.get_compatible(&version) {
    handler(req).await
}
```

### Version-Specific Route Implementation

```rust
use armature_core::{HttpRequest, HttpResponse, Error};

// V1 implementation
async fn get_users_v1(req: HttpRequest) -> Result<HttpResponse, Error> {
    // Simple user list
    let users: Vec<UserV1> = db.get_users().await?;
    HttpResponse::ok().with_json(&users)
}

// V2 implementation with pagination
async fn get_users_v2(req: HttpRequest) -> Result<HttpResponse, Error> {
    let page = req.query("page")
        .and_then(|p| p.parse().ok())
        .unwrap_or(1);

    let users: PaginatedResult<UserV2> = db.get_users_paginated(page).await?;
    HttpResponse::ok().with_json(&users)
}

// V3 implementation with cursor-based pagination
async fn get_users_v3(req: HttpRequest) -> Result<HttpResponse, Error> {
    let cursor = req.query("cursor");

    let result: CursorResult<UserV3> = db.get_users_cursor(cursor).await?;
    HttpResponse::ok().with_json(&result)
}
```

## Request and Response Extensions

### VersionedRequest Trait

```rust
pub trait VersionedRequest {
    /// Extract version using strategy
    fn api_version(&self, strategy: &VersioningStrategy) -> Option<ApiVersion>;

    /// Extract from URL path
    fn url_version(&self) -> Option<ApiVersion>;

    /// Extract from header
    fn header_version(&self, header_name: &str) -> Option<ApiVersion>;

    /// Extract from query parameter
    fn query_version(&self, param_name: &str) -> Option<ApiVersion>;
}
```

### VersionedResponse Trait

```rust
pub trait VersionedResponse {
    /// Add X-API-Version header
    fn with_api_version(self, version: ApiVersion) -> Self;

    /// Add deprecation warning headers
    fn with_deprecation_warning(self, message: &str) -> Self;

    /// Add X-API-Supported-Versions header
    fn with_supported_versions(self, versions: &[ApiVersion]) -> Self;

    /// Add Sunset header (RFC 8594)
    fn with_sunset_date(self, date: &str) -> Self;
}
```

### Response Headers

When `add_response_headers` is enabled:

```http
HTTP/1.1 200 OK
X-API-Version: 2
X-API-Supported-Versions: 1, 2, 3
```

For deprecated versions:

```http
HTTP/1.1 200 OK
X-API-Version: 1
X-API-Deprecated: true
X-API-Deprecation-Message: API version 1 is deprecated
Deprecation: true
```

With sunset date:

```http
HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2024 23:59:59 GMT
```

## Migration Strategies

### Gradual Deprecation

1. **Announce deprecation** (add warning headers)
2. **Set sunset date** (RFC 8594)
3. **Monitor usage** (log deprecated version calls)
4. **Remove support** (return 410 Gone)

```rust
let config = VersionConfig::new(VersioningStrategy::url_path())
    .supported_versions([ApiVersion::V1, ApiVersion::V2, ApiVersion::V3])
    .deprecated(ApiVersion::V1);

async fn handle(req: HttpRequest) -> Result<HttpResponse, Error> {
    let version = config.resolve_version(&req)?;
    let response = process(&req, &version)?;

    // Add sunset date for v1
    if version == ApiVersion::V1 {
        return Ok(response
            .with_api_version(version)
            .with_deprecation_warning("v1 will be removed on 2025-01-01")
            .with_sunset_date("Sat, 01 Jan 2025 00:00:00 GMT"));
    }

    Ok(config.apply_headers(response, &version))
}
```

### Version Compatibility

```rust
use armature_core::versioning::VersionConstraint;

// Accept any v2.x
let constraint = VersionConstraint::range(
    ApiVersion::new(2, 0),
    ApiVersion::new(2, 99),
);

if constraint.matches(&requested_version) {
    // Handle request
}
```

## Best Practices

### 1. Choose the Right Strategy

| Strategy | Pros | Cons | Use When |
|----------|------|------|----------|
| URL Path | Visible, cacheable | URL pollution | Public APIs |
| Header | Clean URLs | Less discoverable | Internal APIs |
| Query Param | Easy to test | Can be stripped | Development |
| Media Type | RESTful | Complex | HATEOAS APIs |

### 2. Start with URL Path

```rust
// Most widely understood
let strategy = VersioningStrategy::url_path();

// URLs like: /v1/users, /v2/users
```

### 3. Always Set a Default Version

```rust
let config = VersionConfig::new(strategy)
    .default_version(ApiVersion::V1);  // Don't break existing clients
```

### 4. Document Deprecation Timeline

```rust
fn deprecated_response(response: HttpResponse) -> HttpResponse {
    response
        .with_deprecation_warning("v1 deprecated since 2024-01-01")
        .with_sunset_date("Sat, 01 Jul 2024 00:00:00 GMT")
}
```

### 5. Use Semantic Versioning

```rust
// Major.Minor format
let version = ApiVersion::new(2, 1);  // v2.1

// Breaking changes → increment major
// Non-breaking additions → increment minor
```

### 6. Maintain Version Parity

Keep similar functionality across versions when possible:

```rust
// Both versions return users, just different formats
async fn get_users_v1(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
async fn get_users_v2(req: HttpRequest) -> Result<HttpResponse, Error> { ... }
```

### 7. Version Your Data Models

```rust
// Separate models per version
mod v1 {
    #[derive(Serialize)]
    pub struct User {
        pub id: u64,
        pub name: String,
    }
}

mod v2 {
    #[derive(Serialize)]
    pub struct User {
        pub id: u64,
        pub first_name: String,
        pub last_name: String,
        pub email: String,
    }
}
```

## Common Pitfalls

### ❌ Breaking Changes in Minor Versions

```rust
// Bad: Breaking change in v1.1
let v1_0 = ApiVersion::new(1, 0);
let v1_1 = ApiVersion::new(1, 1);  // Changed field name - breaking!

// Good: Use v2 for breaking changes
let v2 = ApiVersion::new(2, 0);
```

### ❌ No Default Version

```rust
// Bad: Fails for clients without version
let config = VersionConfig::new(strategy)
    .require_version(true);  // Forces all clients to upgrade

// Good: Graceful fallback
let config = VersionConfig::new(strategy)
    .default_version(ApiVersion::V1);
```

### ❌ Removing Versions Too Quickly

```rust
// Bad: Sudden removal
supported_versions: [ApiVersion::V3]  // V1, V2 clients break!

// Good: Gradual deprecation
supported_versions: [ApiVersion::V1, ApiVersion::V2, ApiVersion::V3]
deprecated: [ApiVersion::V1]  // Warn first
```

## API Reference

### Types

| Type | Description |
|------|-------------|
| `ApiVersion` | Version number (major.minor) |
| `VersioningStrategy` | Strategy for extracting version |
| `VersionConfig` | Configuration for version handling |
| `VersionConstraint` | Version matching rules |
| `VersionedHandler<T>` | Version-specific handler registry |
| `VersionParseError` | Error parsing version string |

### Constants

```rust
ApiVersion::V1  // Version 1.0
ApiVersion::V2  // Version 2.0
ApiVersion::V3  // Version 3.0
```

### Traits

| Trait | Description |
|-------|-------------|
| `VersionedRequest` | Request version extraction methods |
| `VersionedResponse` | Response version header methods |

## Summary

**Key Points:**

1. **Choose a strategy** - URL path is most common for public APIs
2. **Configure properly** - Set default version and supported versions
3. **Handle deprecation gracefully** - Use warning headers and sunset dates
4. **Version your models** - Keep separate DTOs per version
5. **Document changes** - Maintain clear changelog

**Quick Reference:**

```rust
use armature_core::versioning::{
    ApiVersion, VersionConfig, VersioningStrategy,
    VersionedRequest, VersionedResponse
};

// Setup
let config = VersionConfig::new(VersioningStrategy::url_path())
    .default_version(ApiVersion::V1)
    .supported_versions([ApiVersion::V1, ApiVersion::V2])
    .deprecated(ApiVersion::V1);

// Extract version
let version = config.resolve_version(&req)?;

// Version-specific handling
match version.major {
    1 => handle_v1(req).await,
    2 => handle_v2(req).await,
    _ => Err(Error::BadRequest("Unsupported version")),
}

// Add response headers
response.with_api_version(version)
```