convex-rust 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +137 -2
  3. package/dist/cli/bin.d.ts +2 -0
  4. package/dist/cli/bin.js +200 -0
  5. package/dist/cli/build.d.ts +146 -0
  6. package/dist/cli/build.js +902 -0
  7. package/dist/cli/demangle.d.ts +7 -0
  8. package/dist/cli/demangle.js +328 -0
  9. package/dist/cli/diagnostics.d.ts +34 -0
  10. package/dist/cli/diagnostics.js +296 -0
  11. package/dist/cli/generate.d.ts +29 -0
  12. package/dist/cli/generate.js +82 -0
  13. package/dist/cli/manifest.d.ts +24 -0
  14. package/dist/cli/manifest.js +75 -0
  15. package/dist/cli/rustApi.d.ts +78 -0
  16. package/dist/cli/rustApi.js +609 -0
  17. package/dist/cli/scaffold.d.ts +22 -0
  18. package/dist/cli/scaffold.js +114 -0
  19. package/dist/cli/shim.d.ts +30 -0
  20. package/dist/cli/shim.js +140 -0
  21. package/dist/cli/size.d.ts +17 -0
  22. package/dist/cli/size.js +73 -0
  23. package/dist/cli/split.d.ts +20 -0
  24. package/dist/cli/split.js +140 -0
  25. package/dist/cli/symbols.d.ts +30 -0
  26. package/dist/cli/symbols.js +133 -0
  27. package/dist/cli/toolchain.d.ts +53 -0
  28. package/dist/cli/toolchain.js +167 -0
  29. package/dist/cli/validators.d.ts +6 -0
  30. package/dist/cli/validators.js +66 -0
  31. package/dist/cli/wasisdk.d.ts +82 -0
  32. package/dist/cli/wasisdk.js +220 -0
  33. package/dist/cli/wasmHints.d.ts +29 -0
  34. package/dist/cli/wasmHints.js +172 -0
  35. package/dist/runtime/base64.d.ts +4 -0
  36. package/dist/runtime/base64.js +77 -0
  37. package/dist/runtime/crypto.d.ts +7 -0
  38. package/dist/runtime/crypto.js +421 -0
  39. package/dist/runtime/fastjson.d.ts +3 -0
  40. package/dist/runtime/fastjson.js +170 -0
  41. package/dist/runtime/host.d.ts +73 -0
  42. package/dist/runtime/host.js +1230 -0
  43. package/dist/runtime/http.d.ts +40 -0
  44. package/dist/runtime/http.js +157 -0
  45. package/dist/runtime/index.d.ts +2 -0
  46. package/dist/runtime/index.js +1 -0
  47. package/dist/runtime/intl.d.ts +5 -0
  48. package/dist/runtime/intl.js +69 -0
  49. package/dist/runtime/tmpfs.d.ts +129 -0
  50. package/dist/runtime/tmpfs.js +624 -0
  51. package/dist/runtime/wasi.d.ts +33 -0
  52. package/dist/runtime/wasi.js +216 -0
  53. package/package.json +57 -4
  54. package/rust/convex_fn/Cargo.toml +32 -0
  55. package/rust/convex_fn/LICENSE +201 -0
  56. package/rust/convex_fn/build.rs +3 -0
  57. package/rust/convex_fn/src/args.rs +416 -0
  58. package/rust/convex_fn/src/crypto.rs +237 -0
  59. package/rust/convex_fn/src/ctx.rs +1044 -0
  60. package/rust/convex_fn/src/dns.rs +356 -0
  61. package/rust/convex_fn/src/error.rs +179 -0
  62. package/rust/convex_fn/src/executor.rs +577 -0
  63. package/rust/convex_fn/src/fanout.rs +106 -0
  64. package/rust/convex_fn/src/fs.rs +19 -0
  65. package/rust/convex_fn/src/function_ref.rs +219 -0
  66. package/rust/convex_fn/src/http.rs +441 -0
  67. package/rust/convex_fn/src/intl.rs +75 -0
  68. package/rust/convex_fn/src/lib.rs +242 -0
  69. package/rust/convex_fn/src/logging.rs +222 -0
  70. package/rust/convex_fn/src/native.rs +360 -0
  71. package/rust/convex_fn/src/net.rs +122 -0
  72. package/rust/convex_fn/src/sys.rs +178 -0
  73. package/rust/convex_fn/src/values.rs +121 -0
  74. package/rust/convex_fn/src/wire.rs +579 -0
  75. package/rust/convex_fn_macros/Cargo.toml +13 -0
  76. package/rust/convex_fn_macros/LICENSE +201 -0
  77. package/rust/convex_fn_macros/src/derive.rs +632 -0
  78. package/rust/convex_fn_macros/src/lib.rs +338 -0
  79. package/rust/convex_reqwest/Cargo.toml +69 -0
  80. package/rust/convex_reqwest/LICENSE +201 -0
  81. package/rust/convex_reqwest/src/blocking.rs +287 -0
  82. package/rust/convex_reqwest/src/body.rs +152 -0
  83. package/rust/convex_reqwest/src/client.rs +359 -0
  84. package/rust/convex_reqwest/src/config_types.rs +150 -0
  85. package/rust/convex_reqwest/src/error.rs +241 -0
  86. package/rust/convex_reqwest/src/into_url.rs +57 -0
  87. package/rust/convex_reqwest/src/lib.rs +42 -0
  88. package/rust/convex_reqwest/src/multipart.rs +140 -0
  89. package/rust/convex_reqwest/src/request.rs +358 -0
  90. package/rust/convex_reqwest/src/response.rs +310 -0
@@ -0,0 +1,632 @@
1
+ //! `#[derive(ConvexArgs)]` / `#[derive(ConvexValue)]`: a static Convex
2
+ //! validator (`convex_fn::ConvexType`) that matches what serde (de)serialises.
3
+ //! The derive reads the `#[serde(...)]` attributes that change the wire shape
4
+ //! (`rename`, `rename_all`, `default`, `skip*`, `tag`, `content`, `untagged`).
5
+
6
+ use proc_macro2::TokenStream;
7
+ use quote::quote;
8
+ use syn::{
9
+ Attribute, Data, DataEnum, DeriveInput, Error, Fields, GenericArgument, Lit, PathArguments,
10
+ Result, Type,
11
+ };
12
+
13
+ #[derive(Clone, Copy, PartialEq, Eq)]
14
+ pub enum Mode {
15
+ /// `ConvexArgs`: the type must be a struct with named fields.
16
+ Args,
17
+ /// `ConvexValue`: structs and enums.
18
+ Value,
19
+ }
20
+
21
+ pub fn expand(input: DeriveInput, mode: Mode) -> Result<TokenStream> {
22
+ let name = &input.ident;
23
+ let container = ContainerAttrs::parse(&input.attrs)?;
24
+ let validator = match &input.data {
25
+ Data::Struct(s) => {
26
+ if mode == Mode::Args && !matches!(s.fields, Fields::Named(_)) {
27
+ return Err(Error::new_spanned(
28
+ &input.ident,
29
+ "`ConvexArgs` needs a struct with named fields (action arguments are an \
30
+ object); use `ConvexValue` for other shapes",
31
+ ));
32
+ }
33
+ struct_validator(&s.fields, &container)?
34
+ }
35
+ Data::Enum(e) => {
36
+ if mode == Mode::Args {
37
+ return Err(Error::new_spanned(
38
+ &input.ident,
39
+ "`ConvexArgs` needs a struct (action arguments are an object); use \
40
+ `ConvexValue` for enums",
41
+ ));
42
+ }
43
+ enum_validator(e, &container)?
44
+ }
45
+ Data::Union(_) => return Err(Error::new_spanned(&input.ident, "unions are not supported")),
46
+ };
47
+
48
+ let mut generics = input.generics.clone();
49
+ for p in generics.type_params_mut() {
50
+ p.bounds.push(syn::parse_quote!(::convex_fn::ConvexType));
51
+ }
52
+ let (impl_g, ty_g, where_g) = generics.split_for_impl();
53
+ Ok(quote! {
54
+ impl #impl_g ::convex_fn::ConvexType for #name #ty_g #where_g {
55
+ const VALIDATOR: ::convex_fn::Validator = #validator;
56
+ }
57
+ })
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // attributes
62
+
63
+ #[derive(Default)]
64
+ struct ContainerAttrs {
65
+ rename_all: Option<RenameRule>,
66
+ tag: Option<String>,
67
+ content: Option<String>,
68
+ untagged: bool,
69
+ transparent: bool,
70
+ }
71
+
72
+ #[derive(Default)]
73
+ struct FieldAttrs {
74
+ rename: Option<String>,
75
+ default: bool,
76
+ skip: bool,
77
+ skip_serializing: bool,
78
+ skip_deserializing: bool,
79
+ skip_serializing_if: bool,
80
+ id: Option<String>,
81
+ any: bool,
82
+ }
83
+
84
+ #[derive(Default)]
85
+ struct VariantAttrs {
86
+ rename: Option<String>,
87
+ rename_all: Option<RenameRule>,
88
+ skip: bool,
89
+ }
90
+
91
+ fn lit_str(meta: &syn::meta::ParseNestedMeta) -> Result<String> {
92
+ let lit: Lit = meta.value()?.parse()?;
93
+ match lit {
94
+ Lit::Str(s) => Ok(s.value()),
95
+ other => Err(Error::new(other.span(), "expected a string literal")),
96
+ }
97
+ }
98
+
99
+ /// `rename = "x"` or `rename(serialize = "x", deserialize = "y")` (uses the
100
+ /// deserialize name: the validator is mostly for arguments).
101
+ fn rename_value(meta: &syn::meta::ParseNestedMeta) -> Result<String> {
102
+ if meta.input.peek(syn::Token![=]) {
103
+ return lit_str(meta);
104
+ }
105
+ let mut ser = None;
106
+ let mut de = None;
107
+ meta.parse_nested_meta(|m| {
108
+ if m.path.is_ident("serialize") {
109
+ ser = Some(lit_str(&m)?);
110
+ } else if m.path.is_ident("deserialize") {
111
+ de = Some(lit_str(&m)?);
112
+ }
113
+ Ok(())
114
+ })?;
115
+ de.or(ser)
116
+ .ok_or_else(|| meta.error("expected `rename = \"..\"`"))
117
+ }
118
+
119
+ /// Skips the value of an attribute we don't care about.
120
+ fn skip_value(meta: &syn::meta::ParseNestedMeta) -> Result<()> {
121
+ if meta.input.peek(syn::Token![=]) {
122
+ let _: syn::Expr = meta.value()?.parse()?;
123
+ } else if meta.input.peek(syn::token::Paren) {
124
+ meta.parse_nested_meta(|m| skip_value(&m))?;
125
+ }
126
+ Ok(())
127
+ }
128
+
129
+ impl ContainerAttrs {
130
+ fn parse(attrs: &[Attribute]) -> Result<Self> {
131
+ let mut out = ContainerAttrs::default();
132
+ for attr in attrs.iter().filter(|a| a.path().is_ident("serde")) {
133
+ attr.parse_nested_meta(|meta| {
134
+ if meta.path.is_ident("rename_all") {
135
+ let s = if meta.input.peek(syn::Token![=]) {
136
+ lit_str(&meta)?
137
+ } else {
138
+ rename_value(&meta)?
139
+ };
140
+ out.rename_all =
141
+ Some(RenameRule::parse(&s).ok_or_else(|| {
142
+ meta.error(format!("unknown `rename_all` rule `{s}`"))
143
+ })?);
144
+ } else if meta.path.is_ident("tag") {
145
+ out.tag = Some(lit_str(&meta)?);
146
+ } else if meta.path.is_ident("content") {
147
+ out.content = Some(lit_str(&meta)?);
148
+ } else if meta.path.is_ident("untagged") {
149
+ out.untagged = true;
150
+ } else if meta.path.is_ident("transparent") {
151
+ out.transparent = true;
152
+ } else if ["from", "into", "try_from", "remote"]
153
+ .iter()
154
+ .any(|k| meta.path.is_ident(k))
155
+ {
156
+ return Err(meta.error(
157
+ "`ConvexArgs`/`ConvexValue` cannot describe a type converted through \
158
+ another type; implement `convex_fn::ConvexType` by hand",
159
+ ));
160
+ } else {
161
+ skip_value(&meta)?;
162
+ }
163
+ Ok(())
164
+ })?;
165
+ }
166
+ Ok(out)
167
+ }
168
+ }
169
+
170
+ impl FieldAttrs {
171
+ fn parse(attrs: &[Attribute]) -> Result<Self> {
172
+ let mut out = FieldAttrs::default();
173
+ for attr in attrs {
174
+ if attr.path().is_ident("serde") {
175
+ attr.parse_nested_meta(|meta| {
176
+ if meta.path.is_ident("rename") {
177
+ out.rename = Some(rename_value(&meta)?);
178
+ } else if meta.path.is_ident("default") {
179
+ skip_value(&meta)?;
180
+ out.default = true;
181
+ } else if meta.path.is_ident("skip") {
182
+ out.skip = true;
183
+ } else if meta.path.is_ident("skip_serializing") {
184
+ out.skip_serializing = true;
185
+ } else if meta.path.is_ident("skip_deserializing") {
186
+ out.skip_deserializing = true;
187
+ } else if meta.path.is_ident("skip_serializing_if") {
188
+ skip_value(&meta)?;
189
+ out.skip_serializing_if = true;
190
+ } else if meta.path.is_ident("flatten") {
191
+ return Err(meta.error(
192
+ "`#[serde(flatten)]` is not supported by `ConvexArgs`/`ConvexValue`; \
193
+ implement `convex_fn::ConvexType` by hand",
194
+ ));
195
+ } else if ["with", "serialize_with", "deserialize_with"]
196
+ .iter()
197
+ .any(|k| meta.path.is_ident(k))
198
+ {
199
+ return Err(meta.error(
200
+ "custom (de)serializers change the wire format; describe the field \
201
+ with `#[convex(any)]` or `#[convex(id = \"table\")]`",
202
+ ));
203
+ } else {
204
+ skip_value(&meta)?;
205
+ }
206
+ Ok(())
207
+ })?;
208
+ } else if attr.path().is_ident("convex") {
209
+ attr.parse_nested_meta(|meta| {
210
+ if meta.path.is_ident("id") {
211
+ out.id = Some(lit_str(&meta)?);
212
+ } else if meta.path.is_ident("any") {
213
+ out.any = true;
214
+ } else {
215
+ return Err(meta.error("expected `id = \"table\"` or `any`"));
216
+ }
217
+ Ok(())
218
+ })?;
219
+ }
220
+ }
221
+ Ok(out)
222
+ }
223
+ }
224
+
225
+ impl VariantAttrs {
226
+ fn parse(attrs: &[Attribute]) -> Result<Self> {
227
+ let mut out = VariantAttrs::default();
228
+ for attr in attrs.iter().filter(|a| a.path().is_ident("serde")) {
229
+ attr.parse_nested_meta(|meta| {
230
+ if meta.path.is_ident("rename") {
231
+ out.rename = Some(rename_value(&meta)?);
232
+ } else if meta.path.is_ident("rename_all") {
233
+ let s = rename_value(&meta)?;
234
+ out.rename_all = RenameRule::parse(&s);
235
+ } else if meta.path.is_ident("skip") {
236
+ out.skip = true;
237
+ } else if meta.path.is_ident("untagged") || meta.path.is_ident("other") {
238
+ return Err(meta.error("unsupported variant attribute"));
239
+ } else {
240
+ skip_value(&meta)?;
241
+ }
242
+ Ok(())
243
+ })?;
244
+ }
245
+ Ok(out)
246
+ }
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // rename rules
251
+
252
+ #[derive(Clone, Copy, PartialEq, Eq, Debug)]
253
+ pub enum RenameRule {
254
+ Lower,
255
+ Upper,
256
+ Pascal,
257
+ Camel,
258
+ Snake,
259
+ ScreamingSnake,
260
+ Kebab,
261
+ ScreamingKebab,
262
+ }
263
+
264
+ impl RenameRule {
265
+ pub fn parse(s: &str) -> Option<Self> {
266
+ Some(match s {
267
+ "lowercase" => Self::Lower,
268
+ "UPPERCASE" => Self::Upper,
269
+ "PascalCase" => Self::Pascal,
270
+ "camelCase" => Self::Camel,
271
+ "snake_case" => Self::Snake,
272
+ "SCREAMING_SNAKE_CASE" => Self::ScreamingSnake,
273
+ "kebab-case" => Self::Kebab,
274
+ "SCREAMING-KEBAB-CASE" => Self::ScreamingKebab,
275
+ _ => return None,
276
+ })
277
+ }
278
+
279
+ /// Applies to a field name (snake_case in Rust).
280
+ pub fn apply_to_field(self, field: &str) -> String {
281
+ match self {
282
+ Self::Lower | Self::Snake => field.to_string(),
283
+ Self::Upper | Self::ScreamingSnake => field.to_ascii_uppercase(),
284
+ Self::Pascal => {
285
+ let mut out = String::new();
286
+ let mut cap = true;
287
+ for c in field.chars() {
288
+ if c == '_' {
289
+ cap = true;
290
+ } else if cap {
291
+ out.push(c.to_ascii_uppercase());
292
+ cap = false;
293
+ } else {
294
+ out.push(c);
295
+ }
296
+ }
297
+ out
298
+ }
299
+ Self::Camel => {
300
+ let p = Self::Pascal.apply_to_field(field);
301
+ p[..1].to_ascii_lowercase() + &p[1..]
302
+ }
303
+ Self::Kebab => field.replace('_', "-"),
304
+ Self::ScreamingKebab => Self::ScreamingSnake.apply_to_field(field).replace('_', "-"),
305
+ }
306
+ }
307
+
308
+ /// Applies to a variant name (PascalCase in Rust).
309
+ pub fn apply_to_variant(self, variant: &str) -> String {
310
+ match self {
311
+ Self::Pascal => variant.to_string(),
312
+ Self::Lower => variant.to_ascii_lowercase(),
313
+ Self::Upper => variant.to_ascii_uppercase(),
314
+ Self::Camel => variant[..1].to_ascii_lowercase() + &variant[1..],
315
+ Self::Snake => {
316
+ let mut s = String::new();
317
+ for (i, c) in variant.char_indices() {
318
+ if i > 0 && c.is_uppercase() {
319
+ s.push('_');
320
+ }
321
+ s.push(c.to_ascii_lowercase());
322
+ }
323
+ s
324
+ }
325
+ Self::ScreamingSnake => Self::Snake.apply_to_variant(variant).to_ascii_uppercase(),
326
+ Self::Kebab => Self::Snake.apply_to_variant(variant).replace('_', "-"),
327
+ Self::ScreamingKebab => Self::ScreamingSnake
328
+ .apply_to_variant(variant)
329
+ .replace('_', "-"),
330
+ }
331
+ }
332
+ }
333
+
334
+ // ---------------------------------------------------------------------------
335
+ // validators
336
+
337
+ fn v_path() -> TokenStream {
338
+ quote!(::convex_fn::Validator)
339
+ }
340
+
341
+ fn leaf(ty: &Type) -> TokenStream {
342
+ quote!(<#ty as ::convex_fn::ConvexType>::VALIDATOR)
343
+ }
344
+
345
+ /// `Some(inner)` if `ty` is `Option<inner>` (any path ending in `Option`).
346
+ fn option_inner(ty: &Type) -> Option<&Type> {
347
+ generic_inner(ty, "Option")
348
+ }
349
+
350
+ fn generic_inner<'a>(ty: &'a Type, name: &str) -> Option<&'a Type> {
351
+ let Type::Path(p) = ty else { return None };
352
+ let seg = p.path.segments.last()?;
353
+ if seg.ident != name {
354
+ return None;
355
+ }
356
+ let PathArguments::AngleBracketed(args) = &seg.arguments else {
357
+ return None;
358
+ };
359
+ match args.args.first()? {
360
+ GenericArgument::Type(t) => Some(t),
361
+ _ => None,
362
+ }
363
+ }
364
+
365
+ /// Validator expression for a field type, honouring `#[convex(id/any)]`.
366
+ fn type_validator(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream> {
367
+ let v = v_path();
368
+ if attrs.any {
369
+ return Ok(quote!(#v::Any));
370
+ }
371
+ let Some(table) = &attrs.id else {
372
+ return Ok(leaf(ty));
373
+ };
374
+ fn walk(ty: &Type, table: &str) -> Result<TokenStream> {
375
+ let v = v_path();
376
+ if let Some(inner) = option_inner(ty) {
377
+ let i = walk(inner, table)?;
378
+ return Ok(quote!(#v::Union(&[#i, #v::Null])));
379
+ }
380
+ if let Some(inner) = generic_inner(ty, "Vec") {
381
+ let i = walk(inner, table)?;
382
+ return Ok(quote!(#v::Array(&#i)));
383
+ }
384
+ if let Type::Path(p) = ty {
385
+ if p.path.is_ident("String") {
386
+ return Ok(quote!(#v::Id(#table)));
387
+ }
388
+ }
389
+ Err(Error::new_spanned(
390
+ ty,
391
+ "`#[convex(id = \"..\")]` goes on a `String` (or an `Option`/`Vec` of one)",
392
+ ))
393
+ }
394
+ walk(ty, table)
395
+ }
396
+
397
+ struct NamedField {
398
+ wire_name: String,
399
+ validator: TokenStream,
400
+ optional: bool,
401
+ }
402
+
403
+ fn named_fields(
404
+ fields: &syn::FieldsNamed,
405
+ rename_all: Option<RenameRule>,
406
+ ) -> Result<Vec<NamedField>> {
407
+ let mut out = Vec::new();
408
+ for f in &fields.named {
409
+ let attrs = FieldAttrs::parse(&f.attrs)?;
410
+ if attrs.skip {
411
+ continue;
412
+ }
413
+ let ident = f.ident.as_ref().unwrap();
414
+ let raw = ident.to_string();
415
+ let raw = raw.strip_prefix("r#").unwrap_or(&raw);
416
+ let wire_name = match (&attrs.rename, rename_all) {
417
+ (Some(r), _) => r.clone(),
418
+ (None, Some(rule)) => rule.apply_to_field(raw),
419
+ (None, None) => raw.to_string(),
420
+ };
421
+ let v = v_path();
422
+ let (validator, optional) = if let Some(inner) = option_inner(&f.ty) {
423
+ let inner_v = type_validator(inner, &attrs)?;
424
+ if attrs.skip_serializing_if {
425
+ (inner_v, true)
426
+ } else {
427
+ // `None` serializes as `null`, a missing field deserializes as `None`.
428
+ (quote!(#v::Union(&[#inner_v, #v::Null])), true)
429
+ }
430
+ } else {
431
+ let optional = attrs.default || attrs.skip_serializing || attrs.skip_deserializing;
432
+ (type_validator(&f.ty, &attrs)?, optional)
433
+ };
434
+ out.push(NamedField {
435
+ wire_name,
436
+ validator,
437
+ optional,
438
+ });
439
+ }
440
+ Ok(out)
441
+ }
442
+
443
+ fn field_tokens(fields: &[NamedField]) -> Vec<TokenStream> {
444
+ fields
445
+ .iter()
446
+ .map(|f| {
447
+ let name = &f.wire_name;
448
+ let v = &f.validator;
449
+ let opt = f.optional;
450
+ quote!(::convex_fn::Field::new(#name, &#v, #opt))
451
+ })
452
+ .collect()
453
+ }
454
+
455
+ fn object_of(fields: &[NamedField]) -> TokenStream {
456
+ let v = v_path();
457
+ let toks = field_tokens(fields);
458
+ quote!(#v::Object(&[#(#toks),*]))
459
+ }
460
+
461
+ fn struct_validator(fields: &Fields, c: &ContainerAttrs) -> Result<TokenStream> {
462
+ let v = v_path();
463
+ match fields {
464
+ Fields::Named(n) => {
465
+ if c.transparent {
466
+ let live: Vec<_> = n.named.iter().collect();
467
+ if live.len() != 1 {
468
+ return Err(Error::new_spanned(n, "transparent needs exactly one field"));
469
+ }
470
+ return Ok(leaf(&live[0].ty));
471
+ }
472
+ Ok(object_of(&named_fields(n, c.rename_all)?))
473
+ }
474
+ Fields::Unnamed(u) if u.unnamed.len() == 1 => {
475
+ let f = u.unnamed.first().unwrap();
476
+ type_validator(&f.ty, &FieldAttrs::parse(&f.attrs)?)
477
+ }
478
+ Fields::Unnamed(u) => Err(Error::new_spanned(
479
+ u,
480
+ "tuple structs serialize as arrays; use a struct with named fields or implement \
481
+ `convex_fn::ConvexType` by hand",
482
+ )),
483
+ Fields::Unit => Ok(quote!(#v::Null)),
484
+ }
485
+ }
486
+
487
+ fn enum_validator(e: &DataEnum, c: &ContainerAttrs) -> Result<TokenStream> {
488
+ let v = v_path();
489
+ let mut members = Vec::new();
490
+ for var in &e.variants {
491
+ let va = VariantAttrs::parse(&var.attrs)?;
492
+ if va.skip {
493
+ continue;
494
+ }
495
+ let raw = var.ident.to_string();
496
+ let name = match (&va.rename, c.rename_all) {
497
+ (Some(r), _) => r.clone(),
498
+ (None, Some(rule)) => rule.apply_to_variant(&raw),
499
+ (None, None) => raw.clone(),
500
+ };
501
+ // The payload (if any) as a validator, plus its named fields for the
502
+ // internally tagged case.
503
+ enum Payload {
504
+ Unit,
505
+ Newtype(TokenStream),
506
+ Named(Vec<NamedField>),
507
+ }
508
+ let payload = match &var.fields {
509
+ Fields::Unit => Payload::Unit,
510
+ Fields::Unnamed(u) if u.unnamed.len() == 1 => {
511
+ let f = u.unnamed.first().unwrap();
512
+ Payload::Newtype(type_validator(&f.ty, &FieldAttrs::parse(&f.attrs)?)?)
513
+ }
514
+ Fields::Unnamed(u) => {
515
+ return Err(Error::new_spanned(
516
+ u,
517
+ "tuple variants serialize as arrays; use a struct variant",
518
+ ))
519
+ }
520
+ Fields::Named(n) => Payload::Named(named_fields(n, va.rename_all)?),
521
+ };
522
+ let tag_field =
523
+ |tag: &str| quote!(::convex_fn::Field::new(#tag, &#v::LiteralStr(#name), false));
524
+ let member =
525
+ if c.untagged {
526
+ match payload {
527
+ Payload::Unit => quote!(#v::Null),
528
+ Payload::Newtype(t) => t,
529
+ Payload::Named(f) => object_of(&f),
530
+ }
531
+ } else if let (Some(tag), Some(content)) = (&c.tag, &c.content) {
532
+ let tf = tag_field(tag);
533
+ match payload {
534
+ Payload::Unit => quote!(#v::Object(&[#tf])),
535
+ Payload::Newtype(t) => {
536
+ quote!(#v::Object(&[#tf, ::convex_fn::Field::new(#content, &#t, false)]))
537
+ }
538
+ Payload::Named(f) => {
539
+ let o = object_of(&f);
540
+ quote!(#v::Object(&[#tf, ::convex_fn::Field::new(#content, &#o, false)]))
541
+ }
542
+ }
543
+ } else if let Some(tag) = &c.tag {
544
+ let tf = tag_field(tag);
545
+ match payload {
546
+ Payload::Unit => quote!(#v::Object(&[#tf])),
547
+ Payload::Named(f) => {
548
+ let rest = field_tokens(&f);
549
+ quote!(#v::Object(&[#tf, #(#rest),*]))
550
+ }
551
+ Payload::Newtype(_) => return Err(Error::new_spanned(
552
+ var,
553
+ "internally tagged enums cannot hold newtype variants here; use a struct \
554
+ variant",
555
+ )),
556
+ }
557
+ } else {
558
+ // Externally tagged.
559
+ match payload {
560
+ Payload::Unit => quote!(#v::LiteralStr(#name)),
561
+ Payload::Newtype(t) => {
562
+ quote!(#v::Object(&[::convex_fn::Field::new(#name, &#t, false)]))
563
+ }
564
+ Payload::Named(f) => {
565
+ let o = object_of(&f);
566
+ quote!(#v::Object(&[::convex_fn::Field::new(#name, &#o, false)]))
567
+ }
568
+ }
569
+ };
570
+ members.push(member);
571
+ }
572
+ Ok(quote!(#v::Union(&[#(#members),*])))
573
+ }
574
+
575
+ #[cfg(test)]
576
+ mod tests {
577
+ use super::*;
578
+
579
+ #[test]
580
+ fn rename_rules() {
581
+ assert_eq!(RenameRule::Camel.apply_to_field("user_id"), "userId");
582
+ assert_eq!(RenameRule::Pascal.apply_to_field("user_id"), "UserId");
583
+ assert_eq!(
584
+ RenameRule::ScreamingSnake.apply_to_field("user_id"),
585
+ "USER_ID"
586
+ );
587
+ assert_eq!(RenameRule::Kebab.apply_to_field("user_id"), "user-id");
588
+ assert_eq!(
589
+ RenameRule::Snake.apply_to_variant("HttpError"),
590
+ "http_error"
591
+ );
592
+ assert_eq!(RenameRule::Camel.apply_to_variant("HttpError"), "httpError");
593
+ assert_eq!(
594
+ RenameRule::ScreamingKebab.apply_to_variant("HttpError"),
595
+ "HTTP-ERROR"
596
+ );
597
+ }
598
+
599
+ fn expand_src(src: &str, mode: Mode) -> std::result::Result<String, String> {
600
+ let input: DeriveInput = syn::parse_str(src).unwrap();
601
+ expand(input, mode)
602
+ .map(|t| t.to_string())
603
+ .map_err(|e| e.to_string())
604
+ }
605
+
606
+ #[test]
607
+ fn args_rejects_enums_and_tuples() {
608
+ let e = expand_src("enum E { A }", Mode::Args).unwrap_err();
609
+ assert!(e.contains("ConvexValue"), "{e}");
610
+ let e = expand_src("struct T(u8, u8);", Mode::Args).unwrap_err();
611
+ assert!(e.contains("named fields"), "{e}");
612
+ }
613
+
614
+ #[test]
615
+ fn rejects_flatten_and_custom_serializers() {
616
+ let e = expand_src("struct S { #[serde(flatten)] a: A }", Mode::Args).unwrap_err();
617
+ assert!(e.contains("flatten"), "{e}");
618
+ let e = expand_src("struct S { #[serde(with = \"x\")] a: A }", Mode::Args).unwrap_err();
619
+ assert!(e.contains("wire format"), "{e}");
620
+ }
621
+
622
+ #[test]
623
+ fn id_attribute_needs_string() {
624
+ let e = expand_src("struct S { #[convex(id = \"t\")] a: u32 }", Mode::Args).unwrap_err();
625
+ assert!(e.contains("String"), "{e}");
626
+ assert!(expand_src(
627
+ "struct S { #[convex(id = \"t\")] a: Option<String> }",
628
+ Mode::Args
629
+ )
630
+ .is_ok());
631
+ }
632
+ }