versatiles_derive 3.2.0

A toolbox for converting, checking and serving map tiles in various formats.
Documentation
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
//! Proc-macro crate for VersaTiles.
//!
//! This crate provides derive macros and attribute macros to assist with decoding VPL data,
//! generating configuration documentation, and adding error context to functions.
//!
//! # Provided macros
//! - `#[derive(VPLDecode)]`: Derive macro to decode VPL data into Rust structs.
//! - `#[derive(ConfigDoc)]`: Derive macro to generate YAML documentation for configuration structs.
//! - `#[context("...")]`: Attribute macro to add error context to functions returning `Result`.

mod args;
mod config_doc;
mod decode_vpl;

use crate::{
	args::Args,
	config_doc::{angle_inner, collect_doc, is_option, is_primitive_like, is_url_path, path_ident, serde_rename},
	decode_vpl::decode_struct,
};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{ToTokens, quote};
use syn::{Fields, parse_macro_input, spanned::Spanned};

/// Derive macro to decode VPL data into Rust structs.
///
/// This macro can be applied to named-field structs to automatically generate decoding logic
/// from VPL (VersaTiles Programming Language) data.
///
/// # Supported Field Types
///
/// Required fields:
/// - `String`, `bool`, `u8`, `[f64; 4]`
///
/// Optional fields:
/// - `Option<bool>`, `Option<String>`, `Option<f32>`, `Option<u8>`, `Option<u16>`, `Option<u32>`
/// - `Option<[f64; 4]>`, `Option<[u8; 3]>`
/// - `Option<TileCompression>`, `Option<TileSchema>`, `Option<TileFormat>`
///
/// Special fields:
/// - `sources: Vec<VPLPipeline>` - for operations that accept child pipelines
#[proc_macro_derive(VPLDecode)]
pub fn decode_vpl(input: TokenStream) -> TokenStream {
	let input = parse_macro_input!(input as syn::DeriveInput);

	let expanded = match input.data.clone() {
		syn::Data::Struct(data_struct) => match decode_struct(input, data_struct) {
			Ok(tokens) => tokens,
			Err(err) => err.to_compile_error(),
		},
		_ => syn::Error::new_spanned(&input, "VPLDecode can only be derived for structs").to_compile_error(),
	};

	TokenStream::from(expanded)
}

/// Derive macro to generate YAML configuration documentation.
///
/// `ConfigDoc` generates a YAML-formatted demo of the configuration struct, including documentation
/// comments and example values. It supports the following attributes:
///
/// - `serde(rename = "...")` to rename keys in the output.
/// - `#[config_demo("...")]` to provide example values for fields.
///
/// Nested structs are rendered recursively, and `Vec<T>` fields are rendered as YAML lists.
///
/// # Example
///
/// Given:
///
/// ```rust
/// use versatiles_derive::ConfigDoc;
///
/// #[derive(ConfigDoc)]
/// struct Config {
///     /// The name of the user.
///     #[config_demo("alice")]
///     name: String,
///
///     /// List of roles.
///     #[config_demo("- admin\n- user")]
///     roles: Vec<String>,
///
///     /// Nested settings.
///     settings: Settings,
/// }
///
/// #[derive(ConfigDoc)]
/// struct Settings {
///     /// Enable feature.
///     #[config_demo("true")]
///     enabled: bool,
/// }
/// ```
///
/// The generated YAML demo might look like:
///
/// ```yaml
/// # The name of the user.
/// username: alice
///
/// # List of roles.
/// roles:
///   - admin
///   - user
///
/// # Nested settings.
/// settings:
///   # Enable feature.
///   enabled: true
/// ```
#[proc_macro_derive(ConfigDoc, attributes(config, config_demo))]
pub fn derive_config_doc(input: TokenStream) -> TokenStream {
	let input = parse_macro_input!(input as syn::DeriveInput);
	// Parse the input struct definition for generating YAML demo output.

	let name = &input.ident;

	// Ensure the macro is only used on structs with named fields.
	let syn::Data::Struct(data) = &input.data else {
		return syn::Error::new(
			input.span(),
			"ConfigDoc can only be derived for structs with named fields",
		)
		.to_compile_error()
		.into();
	};

	// Access the named fields of the struct; these drive the YAML generation.
	let fields = match &data.fields {
		Fields::Named(named) => &named.named,
		_ => {
			return syn::Error::new(
				data.struct_token.span(),
				"ConfigDoc requires a struct with named fields",
			)
			.to_compile_error()
			.into();
		}
	};

	// Collect per‑field metadata used during YAML codegen.
	struct Row {
		key: String,
		ty: syn::Type,
		doc: String,
		is_vec: bool,
		inner_ty_vec: Option<syn::Type>, // Vec<T> -> T
		// Heuristic: treat non-Option/Vec/Map path types as nested
		is_nested_struct: bool,
		demo_value: Option<String>,
	}

	let mut rows = Vec::<Row>::new();
	for f in fields {
		// Process each field, extract its identifier, type, docs, and attributes.
		let ident = f.ident.clone().expect("named field");
		let key = serde_rename(&f.attrs).unwrap_or_else(|| ident.to_string());
		let ty = f.ty.clone();
		let doc = collect_doc(&f.attrs);
		let is_option = is_option(&f.ty);

		let mut is_vec = false;
		let mut is_map = false;
		let mut inner_ty_vec = None;

		// Detect container types so YAML output can render lists or nested objects correctly.
		if let Some(id) = path_ident(&f.ty) {
			let id_s = id.to_string();
			if id_s == "Vec" {
				is_vec = true;
				if let Some(mut inners) = angle_inner(&f.ty)
					&& let Some(inner) = inners.pop()
				{
					inner_ty_vec = Some(inner.clone());
				}
			} else if id_s == "HashMap" {
				is_map = true;
			}
		}

		let is_url_path = is_url_path(&f.ty);

		// Detect custom example values provided via #[config_demo].
		let mut demo_value = None;
		for attr in &f.attrs {
			if attr.path().is_ident("config_demo")
				&& demo_value.is_none()
				&& let Ok(lit) = attr.parse_args::<syn::LitStr>()
			{
				// Prefer positional literal: #[config_demo("...")]
				demo_value = Some(lit.value());
			}
		}

		// Decide whether to treat this field as a nested struct (recursive YAML).
		let is_nested_struct =
			!is_option && !is_vec && !is_map && path_ident(&f.ty).is_some() && !is_primitive_like(&f.ty) && !is_url_path;

		rows.push(Row {
			key,
			ty,
			doc,
			is_vec,
			inner_ty_vec,
			is_nested_struct,
			demo_value,
		});
	}

	// Build code fragments that emit YAML for each field, including indentation and comments.
	let field_yaml_blocks: Vec<_> = rows
		.iter()
		.map(|r| {
			use proc_macro2::TokenStream as TokenStream2;

			// Start generating YAML lines for documentation and keys.
			let key = &r.key;
			let ty = &r.ty;
			let doc = &r.doc;
			let doc_lit = syn::LitStr::new(doc, Span::call_site());
			let demo_value = r.demo_value.as_ref();
			let demo_lit = demo_value.map(|d| syn::LitStr::new(d, Span::call_site()));
			let key_lit = syn::LitStr::new(key, Span::call_site());

			let mut output: TokenStream2 = quote! {
				__s.push_str(&__sp(__indent));
				__s.push('\n');
				for line in #doc_lit.lines() {
					__s.push_str(&__sp(__indent));
					__s.push_str("# ");
					__s.push_str(line);
					__s.push('\n');
				}
				__s.push_str(&__sp(__indent));
				__s.push_str(#key_lit);
				__s.push_str(": ");
			};

			if let Some(demo_lit) = &demo_lit {
				// If a demo value is provided, use it directly.
				output = quote! {
					#output
					__s.push_str(#demo_lit);
				};
			} else if r.is_nested_struct {
				// If the field is itself a struct, recurse into its `demo_yaml_with_indent`.
				output = quote! {
					#output
					__s.push_str("\n");
					__s.push_str(&<#ty>::demo_yaml_with_indent(__indent + 2));
				};
			} else if r.is_vec
				&& let Some(inner) = &r.inner_ty_vec
			{
				// Vectors require iterating example YAML of the inner type and prefixing "- ".
				output = quote! {
					#output
					__s.push_str("\n");
					let __inner = <#inner>::demo_yaml_with_indent(0);
					let mut __first_line_printed = false;
					for __line in __inner.lines() {
						if !__first_line_printed {
							if __line.trim().is_empty() { continue; }
							__s.push_str(&__sp(__indent + 2));
							__s.push_str("- ");
							__first_line_printed = true;
						} else {
							__s.push_str(&__sp(__indent + 4));
						}
						__s.push_str(__line);
						__s.push('\n');
					}
				};
			}
			// Ensure each field's YAML block ends with a newline.
			quote! {
				#output
				if !__s.ends_with('\n') {
					__s.push('\n');
				}
			}
		})
		.collect();

	// Generate the function that recursively walks fields and builds the final YAML string.
	let expanded = quote! {
		impl #name {
			pub(crate) fn demo_yaml_with_indent(__indent: usize) -> String {
				let mut __s = String::new();
				let __sp = |n: usize| -> String { " ".repeat(n) };

				#( {
					#field_yaml_blocks
				} )*

				__s
			}
		}
	};

	TokenStream::from(expanded)
}

/// Attribute macro to add error context to functions returning `Result`.
///
/// This macro wraps the function body to attach additional context to errors using `anyhow::Context`.
///
/// It supports:
/// - **Sync functions** returning `Result`: wraps the body and maps errors with context.
/// - **`async fn` functions** returning `Result`: awaits the async block and maps errors with context.
/// - **Functions lowered by `async_trait`** (returning pinned futures): wraps the returned future and maps errors with context.
///
/// # Examples
///
/// Sync function:
///
/// ```rust
/// use versatiles_derive::context;
/// use anyhow::Result;
/// #[context("failed to process data")]
/// fn process() -> Result<()> {
///     // ...
///     Ok(())
/// }
/// ```
///
/// Async function:
///
/// ```rust
/// use versatiles_derive::context;
/// use anyhow::Result;
/// #[context("failed to fetch data")]
/// async fn fetch() -> Result<String> {
///     // ...
///     Ok("data".to_string())
/// }
/// ```
///
/// Function lowered by `async_trait`:
///
/// ```rust
/// use versatiles_derive::context;
/// use anyhow::Result;
/// use std::pin::Pin;
/// #[context("failed in async trait method")]
/// fn async_trait_method() -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
///     // ...
///     Box::pin(async { Ok(()) })
/// }
/// ```
#[proc_macro_attribute]
pub fn context(args: TokenStream, input: TokenStream) -> TokenStream {
	let Args(move_token, format_args) = parse_macro_input!(args);
	let mut input = parse_macro_input!(input as syn::ItemFn);

	let body = &input.block;
	let return_type = &input.sig.output;
	let err = Ident::new("err", Span::mixed_site());

	let new_body = if input.sig.asyncness.is_some() {
		let return_type = match return_type {
			syn::ReturnType::Default => {
				return syn::Error::new_spanned(input, "function should return Result")
					.to_compile_error()
					.into();
			}
			syn::ReturnType::Type(_, return_type) => return_type,
		};
		let result = Ident::new("result", Span::mixed_site());
		quote! {{
			use ::anyhow::Context as _;
			let #result: #return_type = (async #move_token { #body }).await;
			#result.map_err(|#err| #err.context(format!(#format_args)).into())
		}}
	} else {
		{
			// Heuristic: if the syntactic return type's last path segment is `Pin`, assume `async_trait` lowered fn
			let is_pin_return = matches!(
				&return_type,
				syn::ReturnType::Type(_, ty)
					if matches!(ty.as_ref(),
						syn::Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == "Pin")
					)
			);

			if is_pin_return {
				// async_trait-lowered: return a boxed async block that awaits the inner future and maps the Result
				quote! {{
					use ::anyhow::Context as _;
					let __fut = (|| #return_type { #body })();
					::core::pin::Pin::from(Box::new(async move {
						let __res = __fut.await;
						__res.map_err(|#err| #err.context(format!(#format_args)).into())
					}))
				}}
			} else {
				// Truly sync function returning Result<...>
				let force_fn_once = Ident::new("force_fn_once", Span::mixed_site());
				quote! {{
					use ::anyhow::Context as _;
					let #force_fn_once = ::core::iter::empty::<()>();
					(#move_token || #return_type {
						::core::mem::drop(#force_fn_once);
						#body
					})().map_err(|#err| #err.context(format!(#format_args)).into())
				}}
			}
		}
	};
	input.block.stmts = vec![syn::Stmt::Expr(syn::Expr::Verbatim(new_body), None)];

	input.into_token_stream().into()
}