mirror of
https://github.com/danbulant/convex-macros
synced 2026-05-19 03:58:31 +00:00
feat: improve tests, docs, as_1 fns (#4)
* feat: update tests and docs, add as_1 fns * docs: add metadata to cargo package
This commit is contained in:
parent
326a37b83c
commit
cdf14a9a14
6 changed files with 429 additions and 16 deletions
|
|
@ -2,9 +2,12 @@
|
|||
name = "ragkit_convex_macros"
|
||||
description = "Macros to help make Convex in Rust nice"
|
||||
authors = ["Ragkit <support@ragkit.com>"]
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
documentation = "https://github.com/ragkit/convex-macros"
|
||||
repository = "https://github.com/ragkit/convex-macros"
|
||||
homepage = "https://github.com/ragkit/convex-macros"
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0.53", features = ["full"] }
|
||||
|
|
|
|||
85
README.md
85
README.md
|
|
@ -3,3 +3,88 @@
|
|||
[](https://github.com/ragkit/convex-macros/actions/workflows/ci.yml)
|
||||
|
||||
Macros to help make Convex in Rust nice
|
||||
|
||||
## Installation
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
ragkit_convex_macros = "0.0.2"
|
||||
|
||||
# Required by code this macro generates.
|
||||
anyhow = "1.0.80"
|
||||
convex = "0.6.0"
|
||||
serde = "1.0.185"
|
||||
serde_json = "1.0"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Create models using the same [Convex validator](https://docs.convex.dev/functions/args-validation#convex-values) syntax as your schema definition.
|
||||
|
||||
```rust
|
||||
convex_model!(User {
|
||||
_id: v.id("users"),
|
||||
name: v.string(),
|
||||
age: v.optional(v.int64()),
|
||||
platform: v.union(
|
||||
v.object({
|
||||
platform: v.literal("google"),
|
||||
verified: v.boolean(),
|
||||
}),
|
||||
v.object({
|
||||
platform: v.literal("github"),
|
||||
username: v.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
This generates `pub struct User {}` with various methods to convert from [`convex::Value`](https://docs.rs/convex/0.6.0/convex/enum.Value.html) and to [`serde_json::Value`](https://docs.rs/serde_json/latest/serde_json/enum.Value.html).
|
||||
|
||||
```rust
|
||||
let user = User::from_convex_value(&Value::Object(btreemap! {
|
||||
"_id".into() => Value::String("1234".into()),
|
||||
"name".into() => Value::String("Alice".into()),
|
||||
"age".into() => Value::Int64(42),
|
||||
"platform".into() => Value::Object(btreemap! {
|
||||
"platform".into() => Value::String("github".into()),
|
||||
"username".into() => Value::String("alicecodes".into()),
|
||||
}),
|
||||
}))
|
||||
.expect("it should parse");
|
||||
|
||||
assert_eq!("1234", user._id);
|
||||
assert_eq!("alicecodes", user.platform.as_2().unwrap().username);
|
||||
assert_eq!(
|
||||
json!({
|
||||
"_id": "1234",
|
||||
"name": "Alice",
|
||||
"age": 42,
|
||||
"platform": {
|
||||
"platform": "github",
|
||||
"username": "alicecodes",
|
||||
},
|
||||
}),
|
||||
json!(user),
|
||||
);
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- `let user = User::from_convex_value(value)?;` to parse a value from Convex client
|
||||
- `json!(user)` to serialize as json
|
||||
- Discriminated unions are automatically handled
|
||||
- Helper functions for each branch are also exposed: `user.platform.as_2()?.username`
|
||||
|
||||
## Limitations
|
||||
|
||||
- This is experimental and may not be "production quality", use with caution.
|
||||
- `v.bytes()`, `v.array()`, `v.any()` are not yet supported.
|
||||
- Field names must be valid Rust identifiers, so keywords like `type` cannot be a field name. Map it to `_type`, `kind`, `t`, etc.
|
||||
- Union variant names are always named like: `Variant1`, `Variant2`, etc.
|
||||
- The first acceptable union branch will be used if there are multiples that could validly parse data.
|
||||
- This package generates code that expects `anyhow`, `convex`, `serde`, and `serde_json` to be available.
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
53
src/model.rs
53
src/model.rs
|
|
@ -104,6 +104,7 @@ impl Parse for ConvexField {
|
|||
impl ConvexField {
|
||||
pub fn print(&self) -> Vec<TokenStream> {
|
||||
let struct_name = self.name.to_struct_name();
|
||||
let struct_name_str = struct_name.to_string();
|
||||
let mut structs = Vec::new();
|
||||
let mut impls = Vec::new();
|
||||
let ignore_attributes = quote! {
|
||||
|
|
@ -111,7 +112,7 @@ impl ConvexField {
|
|||
};
|
||||
// Note: We need a custom serialize to avoid unions printing as objects.
|
||||
let enum_struct_attributes = quote! {
|
||||
#[derive(::serde::Deserialize, Clone, Debug)]
|
||||
#[derive(::serde::Deserialize, Clone, Debug, PartialEq)]
|
||||
};
|
||||
|
||||
match &self.t {
|
||||
|
|
@ -127,11 +128,15 @@ impl ConvexField {
|
|||
let mut extract_arms = Vec::new();
|
||||
let mut json_arms: Vec<TokenStream> = Vec::new();
|
||||
let mut serialize_arms: Vec<TokenStream> = Vec::new();
|
||||
let mut as_fns: Vec<TokenStream> = Vec::new();
|
||||
let mut i = 0;
|
||||
for t in types {
|
||||
i += 1;
|
||||
let branch_name =
|
||||
Ident::new(format!("Variant{}", i).as_str(), Span::call_site());
|
||||
let branch_name_str = branch_name.to_string();
|
||||
let as_name =
|
||||
Ident::new(format!("as_{}", i).as_str(), Span::call_site());
|
||||
let full_branch_name = Ident::new(
|
||||
format!("{}Variant{}", struct_name, i).as_str(),
|
||||
Span::call_site(),
|
||||
|
|
@ -150,6 +155,19 @@ impl ConvexField {
|
|||
serialize_arms.push(quote! {
|
||||
| #struct_name::#branch_name => ().serialize(serializer),
|
||||
});
|
||||
as_fns.push(quote! {
|
||||
pub fn #as_name(&self) -> ::core::result::Result<(), ::anyhow::Error> {
|
||||
if let #struct_name::#branch_name = self {
|
||||
::core::result::Result::Ok(())
|
||||
} else {
|
||||
::core::result::Result::Err(::anyhow::anyhow!(
|
||||
"Expected variant {}::{}",
|
||||
#struct_name_str,
|
||||
#branch_name_str,
|
||||
))
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
enum_kinds.push(quote! {
|
||||
#branch_name(#branch_type),
|
||||
|
|
@ -160,6 +178,19 @@ impl ConvexField {
|
|||
serialize_arms.push(quote! {
|
||||
| #struct_name::#branch_name(ref value) => value.serialize(serializer),
|
||||
});
|
||||
as_fns.push(quote! {
|
||||
pub fn #as_name(&self) -> ::core::result::Result<#branch_type, ::anyhow::Error> {
|
||||
if let #struct_name::#branch_name(value) = self {
|
||||
::core::result::Result::Ok(value.clone())
|
||||
} else {
|
||||
::core::result::Result::Err(::anyhow::anyhow!(
|
||||
"Expected variant {}::{}",
|
||||
#struct_name_str,
|
||||
#branch_name_str,
|
||||
))
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
| None => {
|
||||
|
|
@ -172,6 +203,20 @@ impl ConvexField {
|
|||
serialize_arms.push(quote! {
|
||||
| #struct_name::#branch_name(ref value) => value.serialize(serializer),
|
||||
});
|
||||
// TODO: Probably doing too much cloning.
|
||||
as_fns.push(quote! {
|
||||
pub fn #as_name(&self) -> ::core::result::Result<#full_branch_name, ::anyhow::Error> {
|
||||
if let #struct_name::#branch_name(value) = self {
|
||||
::core::result::Result::Ok(value.clone())
|
||||
} else {
|
||||
::core::result::Result::Err(::anyhow::anyhow!(
|
||||
"Expected variant {}::{}",
|
||||
#struct_name_str,
|
||||
#branch_name_str,
|
||||
))
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
match t {
|
||||
|
|
@ -282,6 +327,10 @@ impl ConvexField {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
#(
|
||||
#as_fns
|
||||
)*
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -334,7 +383,7 @@ impl ConvexField {
|
|||
#[allow(non_snake_case)]
|
||||
};
|
||||
let struct_attributes = quote! {
|
||||
#[derive(::serde::Serialize, ::serde::Deserialize, Clone, Debug)]
|
||||
#[derive(::serde::Serialize, ::serde::Deserialize, Clone, Debug, PartialEq)]
|
||||
};
|
||||
let mut structs = Vec::new();
|
||||
let mut rendered_fields = Vec::new();
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@ use maplit::btreemap;
|
|||
use ragkit_convex_macros::convex_model;
|
||||
use serde_json::json;
|
||||
|
||||
convex_model!(Example {
|
||||
one: v.id("table"),
|
||||
two: v.null(),
|
||||
three: v.int64(),
|
||||
four: v.number(),
|
||||
five: v.boolean(),
|
||||
six: v.string(),
|
||||
seven: v.literal("seven"),
|
||||
eight: v.literal(false),
|
||||
nine: v.literal(9),
|
||||
ten: v.union(v.string(), v.number()),
|
||||
});
|
||||
|
||||
#[test]
|
||||
fn example() {
|
||||
convex_model!(Example {
|
||||
one: v.id("table"),
|
||||
two: v.null(),
|
||||
three: v.int64(),
|
||||
four: v.number(),
|
||||
five: v.boolean(),
|
||||
six: v.string(),
|
||||
seven: v.literal("seven"),
|
||||
eight: v.literal(false),
|
||||
nine: v.literal(9),
|
||||
ten: v.union(v.string(), v.number()),
|
||||
});
|
||||
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"one".into() => Value::String("123fakeid".into()),
|
||||
"two".into() => Value::Null,
|
||||
|
|
@ -114,3 +114,48 @@ fn example3() {
|
|||
let actual_json_data = json!(model);
|
||||
assert_eq!(expected_json_data, actual_json_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readme() {
|
||||
convex_model!(User {
|
||||
_id: v.id("users"),
|
||||
name: v.string(),
|
||||
age: v.optional(v.int64()),
|
||||
platform: v.union(
|
||||
v.object({
|
||||
platform: v.literal("google"),
|
||||
verified: v.boolean(),
|
||||
}),
|
||||
v.object({
|
||||
platform: v.literal("github"),
|
||||
username: v.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
let user = User::from_convex_value(&Value::Object(btreemap! {
|
||||
"_id".into() => Value::String("1234".into()),
|
||||
"name".into() => Value::String("Alice".into()),
|
||||
"age".into() => Value::Int64(42),
|
||||
"platform".into() => Value::Object(btreemap! {
|
||||
"platform".into() => Value::String("github".into()),
|
||||
"username".into() => Value::String("alicecodes".into()),
|
||||
}),
|
||||
}))
|
||||
.expect("it should parse");
|
||||
|
||||
assert_eq!("1234", user._id);
|
||||
assert_eq!("alicecodes", user.platform.as_2().unwrap().username);
|
||||
assert_eq!(
|
||||
json!({
|
||||
"_id": "1234",
|
||||
"name": "Alice",
|
||||
"age": 42,
|
||||
"platform": {
|
||||
"platform": "github",
|
||||
"username": "alicecodes",
|
||||
},
|
||||
}),
|
||||
json!(user),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
167
tests/strings.rs
Normal file
167
tests/strings.rs
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
use convex::Value;
|
||||
use maplit::btreemap;
|
||||
use ragkit_convex_macros::convex_model;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn basic_string() {
|
||||
convex_model!(Model { a: v.string() });
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::String("apple".into()),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": "apple",
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!("apple", model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_string_negative() {
|
||||
convex_model!(Model { a: v.string() });
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"a".into() => Value::Int64(42),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"b".into() => Value::String("apple".into()),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_id() {
|
||||
convex_model!(Model { a: v.id("apples") });
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::String("1234".into()),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": "1234",
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!("1234", model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_id_negative() {
|
||||
convex_model!(Model { a: v.id("apples") });
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"a".into() => Value::Int64(42),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"b".into() => Value::String("apple".into()),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_string_literal() {
|
||||
convex_model!(Model { a: v.literal("apples") });
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::String("apples".into()),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": "apples",
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!("apples", model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_string_literal_negative() {
|
||||
convex_model!(Model { a: v.literal("apples") });
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"a".into() => Value::String("not-apple".into()),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"a".into() => Value::Int64(42),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
|
||||
let model = Model::from_convex_value(&Value::Object(btreemap! {
|
||||
"b".into() => Value::String("apple".into()),
|
||||
}));
|
||||
assert!(model.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_optional_string() {
|
||||
convex_model!(Model { a: v.optional(v.string()) });
|
||||
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::String("apples".into()),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": "apples",
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!(Some("apples".into()), model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::Null,
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": null,
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!(None, model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_string_union() {
|
||||
convex_model!(Model { a: v.union(v.literal("apples"), v.literal("banana")) });
|
||||
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::String("apples".into()),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": "apples",
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!(ModelA::Variant1("apples".into()), model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::String("banana".into()),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": "banana",
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
assert_eq!(ModelA::Variant2("banana".into()), model.a);
|
||||
assert_eq!(json_data, json!(model));
|
||||
}
|
||||
64
tests/unions.rs
Normal file
64
tests/unions.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use convex::Value;
|
||||
use maplit::btreemap;
|
||||
use ragkit_convex_macros::convex_model;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn basic_discriminated() {
|
||||
convex_model!(Model {
|
||||
a: v.union(
|
||||
v.object({
|
||||
t: v.literal("one"),
|
||||
value: v.int64(),
|
||||
}),
|
||||
v.object({
|
||||
t: v.literal("two"),
|
||||
value: v.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Try variant 1
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::Object(btreemap! {
|
||||
"t".into() => Value::String("one".into()),
|
||||
"value".into() => Value::Int64(42),
|
||||
}),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": {
|
||||
"t": "one",
|
||||
"value": 42,
|
||||
},
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
let a = model.a.as_1().unwrap();
|
||||
assert_eq!("one", a.t);
|
||||
assert_eq!(42, a.value);
|
||||
assert_eq!(json_data, json!(model));
|
||||
|
||||
// Try Variant 2
|
||||
let convex_data = Value::Object(btreemap! {
|
||||
"a".into() => Value::Object(btreemap! {
|
||||
"t".into() => Value::String("two".into()),
|
||||
"value".into() => Value::String("something".into()),
|
||||
}),
|
||||
});
|
||||
let json_data = json!({
|
||||
"a": {
|
||||
"t": "two",
|
||||
"value": "something",
|
||||
},
|
||||
});
|
||||
|
||||
let model = Model::from_convex_value(&convex_data);
|
||||
assert!(model.is_ok());
|
||||
let model = model.unwrap();
|
||||
let a = model.a.as_2().unwrap();
|
||||
assert_eq!("two", a.t);
|
||||
assert_eq!("something", a.value);
|
||||
assert_eq!(json_data, json!(model));
|
||||
}
|
||||
Loading…
Reference in a new issue