v08_features/
v08_features.rs

1//! Demonstrates v0.8 features: schema composition, metadata, and vendor extensions.
2//!
3//! This example showcases:
4//! 1. anyOf/allOf/oneOf composition
5//! 2. default/example/examples metadata
6//! 3. readOnly/writeOnly/deprecated modifiers
7//! 4. Vendor extensions for non-mappable validations
8
9use domainstack_schema::{OpenApiBuilder, Schema, ToSchema};
10use serde_json::json;
11
12// === 1. Schema Composition ===
13
14/// Payment method: either card OR cash (union type with anyOf)
15#[allow(dead_code)]
16struct PaymentMethod {
17    method_type: String,
18}
19
20impl ToSchema for PaymentMethod {
21    fn schema_name() -> &'static str {
22        "PaymentMethod"
23    }
24
25    fn schema() -> Schema {
26        Schema::any_of(vec![
27            Schema::object()
28                .property("type", Schema::string().enum_values(&["card"]))
29                .property("cardNumber", Schema::string().min_length(16).max_length(16))
30                .property("cvv", Schema::string().min_length(3).max_length(4))
31                .required(&["type", "cardNumber", "cvv"]),
32            Schema::object()
33                .property("type", Schema::string().enum_values(&["cash"]))
34                .property("amount", Schema::number().minimum(0))
35                .required(&["type", "amount"]),
36        ])
37    }
38}
39
40/// Admin user: inherits from User AND adds admin field (composition with allOf)
41#[allow(dead_code)]
42struct AdminUser {
43    // Inherits all User fields + additional fields
44    admin: bool,
45}
46
47impl ToSchema for AdminUser {
48    fn schema_name() -> &'static str {
49        "AdminUser"
50    }
51
52    fn schema() -> Schema {
53        Schema::all_of(vec![
54            Schema::reference("User"),
55            Schema::object()
56                .property("admin", Schema::boolean().default(json!(false)))
57                .property("permissions", Schema::array(Schema::string()))
58                .required(&["admin"]),
59        ])
60        .description("Admin user with elevated permissions")
61    }
62}
63
64// === 2. Metadata: default, example, examples ===
65
66#[allow(dead_code)]
67struct UserSettings {
68    theme: String,
69    language: String,
70    notifications_enabled: bool,
71}
72
73impl ToSchema for UserSettings {
74    fn schema_name() -> &'static str {
75        "UserSettings"
76    }
77
78    fn schema() -> Schema {
79        Schema::object()
80            .description("User preferences and settings")
81            .property(
82                "theme",
83                Schema::string()
84                    .enum_values(&["light", "dark", "auto"])
85                    .default(json!("auto"))
86                    .example(json!("dark"))
87                    .description("UI theme preference"),
88            )
89            .property(
90                "language",
91                Schema::string()
92                    .default(json!("en"))
93                    .examples(vec![json!("en"), json!("es"), json!("fr")])
94                    .description("Preferred language code (ISO 639-1)"),
95            )
96            .property(
97                "notificationsEnabled",
98                Schema::boolean()
99                    .default(json!(true))
100                    .description("Enable/disable notifications"),
101            )
102            .required(&["theme", "language"])
103    }
104}
105
106// === 3. Request/Response Modifiers: readOnly, writeOnly, deprecated ===
107
108#[allow(dead_code)]
109struct UserAccount {
110    id: String,
111    email: String,
112    password: String,
113    created_at: String,
114    old_username: Option<String>,
115}
116
117impl ToSchema for UserAccount {
118    fn schema_name() -> &'static str {
119        "UserAccount"
120    }
121
122    fn schema() -> Schema {
123        Schema::object()
124            .description("User account with request/response field modifiers")
125            .property(
126                "id",
127                Schema::string()
128                    .read_only(true)
129                    .description("Auto-generated user ID (returned in responses only)"),
130            )
131            .property(
132                "email",
133                Schema::string().format("email").description("User email"),
134            )
135            .property(
136                "password",
137                Schema::string()
138                    .format("password")
139                    .min_length(8)
140                    .write_only(true)
141                    .description("Password (accepted in requests only, never returned)"),
142            )
143            .property(
144                "createdAt",
145                Schema::string()
146                    .format("date-time")
147                    .read_only(true)
148                    .description("Account creation timestamp"),
149            )
150            .property(
151                "oldUsername",
152                Schema::string()
153                    .deprecated(true)
154                    .description("Deprecated: Use 'email' instead"),
155            )
156            .required(&["email", "password"])
157    }
158}
159
160// === 4. Vendor Extensions for Non-Mappable Validations ===
161
162#[allow(dead_code)]
163struct DateRange {
164    start_date: String,
165    end_date: String,
166}
167
168impl ToSchema for DateRange {
169    fn schema_name() -> &'static str {
170        "DateRange"
171    }
172
173    fn schema() -> Schema {
174        Schema::object()
175            .description("Date range with cross-field validation")
176            .property("startDate", Schema::string().format("date"))
177            .property("endDate", Schema::string().format("date"))
178            .required(&["startDate", "endDate"])
179            // Cross-field validation doesn't map to OpenAPI, so use vendor extension
180            .extension(
181                "x-domainstack-validations",
182                json!({
183                    "cross_field": ["endDate > startDate"],
184                    "description": "End date must be after start date"
185                }),
186            )
187    }
188}
189
190#[allow(dead_code)]
191struct OrderForm {
192    total: f64,
193    minimum_order: f64,
194    requires_minimum: bool,
195}
196
197impl ToSchema for OrderForm {
198    fn schema_name() -> &'static str {
199        "OrderForm"
200    }
201
202    fn schema() -> Schema {
203        Schema::object()
204            .description("Order form with conditional validation")
205            .property("total", Schema::number().minimum(0))
206            .property("minimumOrder", Schema::number().minimum(0))
207            .property("requiresMinimum", Schema::boolean())
208            .required(&["total", "minimumOrder", "requiresMinimum"])
209            // Conditional validation doesn't map to OpenAPI
210            .extension(
211                "x-domainstack-validations",
212                json!({
213                    "conditional": {
214                        "when": "requiresMinimum == true",
215                        "then": "total >= minimumOrder"
216                    },
217                    "description": "When minimum is required, total must meet it"
218                }),
219            )
220    }
221}
222
223// === User type for allOf example ===
224
225#[allow(dead_code)]
226struct User {
227    email: String,
228    name: String,
229}
230
231impl ToSchema for User {
232    fn schema_name() -> &'static str {
233        "User"
234    }
235
236    fn schema() -> Schema {
237        Schema::object()
238            .property("email", Schema::string().format("email"))
239            .property("name", Schema::string().min_length(1))
240            .required(&["email", "name"])
241    }
242}
243
244fn main() {
245    let spec = OpenApiBuilder::new("v0.8 Features Demo", "1.0.0")
246        .description("Demonstrates OpenAPI v0.8 features: composition, metadata, and extensions")
247        .register::<PaymentMethod>()
248        .register::<AdminUser>()
249        .register::<User>()
250        .register::<UserSettings>()
251        .register::<UserAccount>()
252        .register::<DateRange>()
253        .register::<OrderForm>()
254        .build();
255
256    println!("{}", spec.to_json().expect("Failed to serialize"));
257
258    println!("\n=== v0.8 Features Demonstrated ===");
259    println!("[ok] anyOf: PaymentMethod (union of card | cash)");
260    println!("[ok] allOf: AdminUser (extends User)");
261    println!("[ok] default: UserSettings.theme = 'auto'");
262    println!("[ok] example: UserSettings.theme example = 'dark'");
263    println!("[ok] examples: UserSettings.language examples = ['en', 'es', 'fr']");
264    println!("[ok] readOnly: UserAccount.id, createdAt (response only)");
265    println!("[ok] writeOnly: UserAccount.password (request only)");
266    println!("[ok] deprecated: UserAccount.oldUsername");
267    println!("[ok] vendor extensions: DateRange, OrderForm (x-domainstack-validations)");
268    println!("\nAll v0.8 features working correctly!");
269}