chore: initial commit

This commit is contained in:
Kyle 2024-03-18 19:05:39 -07:00
commit 0f65ecc1d3
16 changed files with 2748 additions and 0 deletions

30
.gitignore vendored Normal file
View file

@ -0,0 +1,30 @@
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# dependencies
node_modules
.pnp
.pnp.js
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
# local env files
.env
# use pnpm, not npm
.package-lock.json
# vscode settings
.vscode

4
.husky/commit-msg Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm -- commitlint --edit ${1}

4
.husky/pre-commit Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm lint-staged

4
.lintstagedrc.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
"*.rs": "rustfmt",
"*.{js,jsx,ts,tsx,json,yml,yaml,md}": "prettier --write",
};

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
v20.3.1

2
.prettierignore Normal file
View file

@ -0,0 +1,2 @@
node_modules
pnpm-lock.yaml

22
Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "ragkit_convex_macros"
description = "Macros to help make Convex in Rust nice"
authors = ["Ragkit <support@ragkit.com>"]
version = "0.1.0"
edition = "2021"
license = "MIT"
[dependencies]
syn = { version = "2.0.53", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
[lib]
proc-macro = true
[dev-dependencies]
anyhow = "1.0.80"
convex = "0.6.0"
maplit = "1.0.2"
serde = "1.0.185"
serde_json = "1.0"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Ragkit, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# convex-macros
[![CI Badge](https://github.com/ragkit/convex-macros/actions/workflows/ci.yml/badge.svg)](https://github.com/ragkit/convex-macros/actions/workflows/ci.yml)
Macros to help make Convex in Rust nice

1
commitlint.config.js Normal file
View file

@ -0,0 +1 @@
module.exports = { extends: ["@commitlint/config-conventional"] };

17
package.json Normal file
View file

@ -0,0 +1,17 @@
{
"name": "@ragkit/convex-macros",
"version": "0.0.0",
"packageManager": "pnpm@8.15.5",
"scripts": {
"prepare": "husky install",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\"",
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,yml,yaml,md}\""
},
"devDependencies": {
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"husky": "^8.0.0",
"lint-staged": "^13.2.3",
"prettier": "^3.0.0"
}
}

1710
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

27
rustfmt.toml Normal file
View file

@ -0,0 +1,27 @@
max_width = 80
tab_spaces = 2
newline_style = "Unix"
match_arm_leading_pipes = "Always"
match_block_trailing_comma = true
use_field_init_shorthand = true
# Outdated default, the `try!` macro is deprecated now.
use_try_shorthand = true
# Allow all things to be max width.
use_small_heuristics = "Max"
# Unstable features
wrap_comments = true
format_code_in_doc_comments = true
comment_width = 80
format_strings = true
hex_literal_case = "Upper"
where_single_line = true
combine_control_expr = false
# Prevents fighting with format on save.
empty_item_single_line = false
# Import organization
imports_layout = "HorizontalVertical"
imports_granularity = "Module"
group_imports = "StdExternalCrate"

15
src/lib.rs Normal file
View file

@ -0,0 +1,15 @@
extern crate proc_macro;
use model::ConvexField;
use proc_macro::TokenStream;
use syn::parse_macro_input;
mod model;
#[proc_macro]
pub fn convex_model(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as ConvexField);
let output = input.print();
let ts = proc_macro2::TokenStream::from_iter(output);
ts.into()
}

815
src/model.rs Normal file
View file

@ -0,0 +1,815 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::{Parse, ParseBuffer, ParseStream};
use syn::{Error, Ident, Lit, Result, Token};
#[derive(Clone)]
pub struct ConvexName {
pub path: Vec<String>,
pub id: String,
}
#[derive(Clone)]
pub struct ConvexField {
pub name: ConvexName,
pub t: ConvexType,
}
// See: https://docs.convex.dev/functions/args-validation
#[derive(Clone)]
pub enum ConvexType {
// Core types.
Id(String),
Null,
Int64,
Number,
Bool,
String,
// TODO: Bytes,
// TODO: Array(ConvexType),
Object(Vec<ConvexField>),
Union(Vec<ConvexType>),
StringLiteral(String),
BoolLiteral(bool),
IntLiteral(i64),
// TODO: Any,
Optional(Box<ConvexField>),
}
impl ConvexType {
fn print(&self) -> Option<TokenStream> {
match &self {
// TODO: Improve Id types.
| ConvexType::Id(_) => Some(quote! { String }),
| ConvexType::Null => Some(quote! { () }),
| ConvexType::Int64 => Some(quote! { i64 }),
| ConvexType::Number => Some(quote! { f64 }),
| ConvexType::Bool => Some(quote! { bool }),
| ConvexType::String => Some(quote! { String }),
// TODO: Can rust represent literal types?
| ConvexType::StringLiteral(_) => Some(quote! { String }),
| ConvexType::BoolLiteral(_) => Some(quote! { bool }),
| ConvexType::IntLiteral(_) => Some(quote! { i64 }),
// Kinda a weird one, we technically know the full type even if the child
// is an Object or Union, but other parts of the system rely on returning
// None here to communicate the Optional wraps a "complex" type.
| ConvexType::Optional(child) => {
child.t.print().map(|ts| quote! { Option<#ts> })
},
// These depend on field.name to generate a struct name.
| ConvexType::Object(_) => None,
| ConvexType::Union(_) => None,
}
}
}
impl ConvexName {
fn to_struct_name(&self) -> Ident {
let path_parts: Vec<String> =
self.path.iter().map(|p| capitalize_first_char(p)).collect();
let id_part = capitalize_first_char(&self.id);
let s = [path_parts.join(""), id_part].join("");
Ident::new(s.as_str(), Span::call_site())
}
fn to_field_name(&self) -> Ident {
Ident::new(self.id.as_str(), Span::call_site())
}
fn full_path(&self) -> Vec<String> {
let mut v = self.path.clone();
v.push(self.id.clone());
v
}
}
impl Parse for ConvexField {
fn parse(input: ParseStream) -> Result<Self> {
let ident = Ident::parse(input)?;
let name = ConvexName { path: Vec::new(), id: ident.to_string() };
let content;
let _ = syn::braced!(content in input);
let ts = Self::parse_comma_separated(&name, &content, |name, b| {
Self::parse_child(name, b)
})?;
Ok(Self { name, t: ConvexType::Object(ts) })
}
}
impl ConvexField {
pub fn print(&self) -> Vec<TokenStream> {
let struct_name = self.name.to_struct_name();
let mut structs = Vec::new();
let mut impls = Vec::new();
let ignore_attributes = quote! {
#[allow(non_snake_case)]
};
let struct_attributes = quote! {
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
};
match &self.t {
| ConvexType::Object(fields) => {
structs.append(&mut Self::print_structs(fields, &struct_name));
impls.push(Self::print_to_json_impl(fields, &struct_name));
impls.push(Self::print_from_convex_value(fields, &struct_name));
},
| ConvexType::Union(types) => {
let field_name = self.name.to_field_name();
let field_name_str = field_name.to_string();
let mut enum_kinds = Vec::new();
let mut extract_arms = Vec::new();
let mut json_arms: 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 full_branch_name = Ident::new(
format!("{}Variant{}", struct_name, i).as_str(),
Span::call_site(),
);
let branch_type = t.print();
match branch_type {
| Some(branch_type) => {
// TODO: Clean up hard-coding of unit type in unions.
if branch_type.to_string() == "()" {
enum_kinds.push(quote! {
#branch_name,
});
json_arms.push(quote! {
| #struct_name::#branch_name => ::serde_json::Value::Null,
});
} else {
enum_kinds.push(quote! {
#branch_name(#branch_type),
});
json_arms.push(quote! {
| #struct_name::#branch_name(value) => ::serde_json::json!(value),
});
}
},
| None => {
enum_kinds.push(quote! {
#branch_name(#full_branch_name),
});
json_arms.push(quote! {
| #struct_name::#branch_name(value) => ::serde_json::json!(value),
});
},
};
match t {
| ConvexType::Id(_) => {
extract_arms.push(quote! {
| ::convex::Value::String(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
});
},
| ConvexType::Null => extract_arms.push(quote! {
| ::convex::Value::Null => {
::core::result::Result::Ok(#struct_name::#branch_name)
},
}),
// TODO: Should this accept Float64 or just Int64?
| ConvexType::Int64 => extract_arms.push(quote! {
| ::convex::Value::Int64(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
| ::convex::Value::Float64(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone() as i64))
},
}),
| ConvexType::IntLiteral(i) => extract_arms.push(quote! {
| ::convex::Value::Int64(value) if value.clone() == #i => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
| ::convex::Value::Float64(value) if value.clone() as i64 == #i => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone() as i64))
},
}),
// TODO: Should this accept Int64 or just Float64?
| ConvexType::Number => extract_arms.push(quote! {
| ::convex::Value::Int64(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone() as f64))
},
| ::convex::Value::Float64(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
}),
| ConvexType::Bool => extract_arms.push(quote! {
| ::convex::Value::Boolean(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
}),
| ConvexType::BoolLiteral(b) => extract_arms.push(quote! {
| ::convex::Value::Boolean(value) if value.clone() == #b => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
}),
| ConvexType::String => extract_arms.push(quote! {
| ::convex::Value::String(value) => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
}),
| ConvexType::StringLiteral(s) => extract_arms.push(quote! {
| ::convex::Value::String(value) if value == #s => {
::core::result::Result::Ok(#struct_name::#branch_name(value.clone()))
},
}),
| ConvexType::Object(fields) => {
structs.append(&mut Self::print_structs(fields, &full_branch_name));
impls.push(Self::print_from_convex_value(fields, &full_branch_name));
extract_arms.push(quote! {
| value if #full_branch_name::from_convex_value(value).is_ok() => {
Ok(#struct_name::#branch_name(#full_branch_name::from_convex_value(value)?))
}
});
},
| ConvexType::Optional(_) => panic!("Unions may not contain optional branches"),
| ConvexType::Union(_) => panic!("Unions may not directly contain other unions, put other types between them"),
};
}
structs.push(quote! {
#ignore_attributes
#struct_attributes
pub enum #struct_name {
#( #enum_kinds )*
}
});
impls.push(quote! {
#ignore_attributes
impl #struct_name {
fn from_convex_value(
value: &::convex::Value
) -> ::core::result::Result<Self, ::anyhow::Error> {
match value {
#( #extract_arms )*
| _ => {
Err(::anyhow::anyhow!("Invalid union type for '{}'", #field_name_str))
},
}
}
}
});
impls.push(quote! {
#ignore_attributes
impl ::core::convert::From<#struct_name> for ::serde_json::Value {
fn from(value: #struct_name) -> Self {
match value {
#( #json_arms )*
}
}
}
})
},
| _ => {
panic!(
"Internal Error: Should have a complex field like object or union"
)
},
}
[structs, impls].concat()
}
fn print_from_convex_value(
fields: &Vec<ConvexField>,
struct_name: &Ident,
) -> TokenStream {
let ignore_attributes = quote! {
#[allow(non_snake_case)]
};
let extract_ts = Self::print_extract_fields(fields, struct_name);
quote! {
#ignore_attributes
impl #struct_name {
fn from_convex_value(
value: &::convex::Value
) -> ::core::result::Result<Self, ::anyhow::Error> {
#extract_ts
}
}
}
}
fn print_structs(
fields: &Vec<ConvexField>,
struct_name: &Ident,
) -> Vec<TokenStream> {
let ignore_attributes = quote! {
#[allow(non_snake_case)]
};
let struct_attributes = quote! {
#[derive(serde::Deserialize, serde::Serialize, Clone, Debug)]
};
let mut structs = Vec::new();
let mut rendered_fields = Vec::new();
for field in fields {
let field_name = field.name.to_field_name();
match field.t.print() {
| Some(field_type) => rendered_fields.push(quote! {
pub #field_name: #field_type,
}),
| None => {
// TODO: This is hacky hard-codes nesting
if let ConvexType::Optional(child) = &field.t {
let struct_name = field.name.to_struct_name();
let field_type = quote! { Option<#struct_name> };
let mut child_struct = child.print();
structs.append(&mut child_struct);
rendered_fields.push(quote! {
pub #field_name: #field_type,
});
} else {
let field_type = field.name.to_struct_name();
let mut child_struct = field.print();
structs.append(&mut child_struct);
rendered_fields.push(quote! {
pub #field_name: #field_type,
});
}
},
};
}
structs.push(quote! {
#ignore_attributes
#struct_attributes
pub struct #struct_name {
#( #rendered_fields )*
}
});
structs
}
fn print_to_json_impl(
fields: &Vec<ConvexField>,
struct_name: &Ident,
) -> TokenStream {
let ignore_attributes = quote! {
#[allow(non_snake_case)]
};
let mut json_fields = Vec::new();
for field in fields {
let field_name = field.name.to_field_name();
let field_name_str = field_name.to_string();
json_fields.push(quote! {
#field_name_str: value.#field_name,
});
}
quote! {
#ignore_attributes
impl ::core::convert::From<#struct_name> for ::serde_json::Value {
fn from(value: #struct_name) -> Self {
::serde_json::json!({
#( #json_fields )*
})
}
}
}
}
fn print_extract_fields(
fields: &Vec<ConvexField>,
struct_name: &Ident,
) -> TokenStream {
let mut extract_fields = Vec::new();
let mut field_idents = Vec::new();
for field in fields {
extract_fields.push(Self::print_extract_field(field, None));
let field_name = field.name.to_field_name();
field_idents.push(quote! {
#field_name,
});
}
quote! {
match value {
| ::convex::Value::Object(object) => {
#( #extract_fields )*
Ok(#struct_name {
#( #field_idents )*
})
},
| _ => {
return Err(::anyhow::anyhow!("Expected an object"));
},
}
}
}
fn print_extract_field(
field: &ConvexField,
match_ident: Option<Ident>,
) -> TokenStream {
let field_name = field.name.to_field_name();
let field_name_str = field_name.to_string();
let match_target = match match_ident {
| Some(ident) => quote! { #ident },
| None => quote! { object.get(#field_name_str) },
};
match &field.t {
| ConvexType::Object(_) => {
let struct_name = field.name.to_struct_name();
quote! {
let #field_name = match #match_target {
| ::core::option::Option::Some(value) => {
#struct_name::from_convex_value(value)?
},
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be an object", #field_name_str));
},
};
}
},
| ConvexType::Union(_) => {
let struct_name = field.name.to_struct_name();
quote! {
let #field_name = match #match_target {
| ::core::option::Option::Some(value) => {
#struct_name::from_convex_value(value)?
},
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to match union", #field_name_str));
},
};
}
},
| _ => Self::print_extract_type(
&field.t,
field_name,
match_target,
field_name_str,
),
}
}
fn print_extract_type(
t: &ConvexType,
ident: Ident,
match_target: TokenStream,
error_name: String,
) -> TokenStream {
match &t {
| ConvexType::Id(_) | ConvexType::String => {
Self::print_extract_string(ident, match_target, error_name)
},
| ConvexType::Null => {
Self::print_extract_null(ident, match_target, error_name)
},
| ConvexType::Int64 => {
Self::print_extract_int(ident, match_target, error_name)
},
| ConvexType::Number => {
Self::print_extract_number(ident, match_target, error_name)
},
| ConvexType::Bool => {
Self::print_extract_bool(ident, match_target, error_name)
},
| ConvexType::IntLiteral(literal) => Self::print_extract_int_literal(
ident,
match_target,
error_name,
*literal,
),
| ConvexType::BoolLiteral(literal) => Self::print_extract_bool_literal(
ident,
match_target,
error_name,
*literal,
),
| ConvexType::StringLiteral(literal) => {
Self::print_extract_string_literal(
ident,
match_target,
error_name,
literal.into(),
)
},
| ConvexType::Optional(next_t) => {
let next_target = Ident::new("value", Span::call_site());
let child_match = Self::print_extract_field(next_t, Some(next_target));
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Null) => ::core::option::Option::None,
| ::core::option::Option::None => ::core::option::Option::None,
| value => {
#child_match
::core::option::Option::Some(#ident)
},
};
}
},
| _ => {
panic!("Unimplemented print_extract_type")
},
}
}
fn print_extract_bool(
ident: Ident,
match_target: TokenStream,
error_name: String,
) -> TokenStream {
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Boolean(b)) => b.clone(),
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be a boolean", #error_name));
},
};
}
}
fn print_extract_int(
ident: Ident,
match_target: TokenStream,
error_name: String,
) -> TokenStream {
// TODO: Should this work for both Ints and Floats?
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Int64(i)) => i.clone(),
| ::core::option::Option::Some(::convex::Value::Float64(f)) => f.clone() as i64,
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be an int", #error_name));
},
};
}
}
fn print_extract_null(
ident: Ident,
match_target: TokenStream,
error_name: String,
) -> TokenStream {
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Null) => (),
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be null", #error_name));
},
};
}
}
fn print_extract_string(
ident: Ident,
match_target: TokenStream,
error_name: String,
) -> TokenStream {
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::String(value)) => value.clone(),
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be a string", #error_name));
},
};
}
}
fn print_extract_int_literal(
ident: Ident,
match_target: TokenStream,
error_name: String,
literal: i64,
) -> TokenStream {
// TODO: Should this support both float and int?
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Float64(value)) => {
let v = value.clone() as i64;
if v != #literal {
return Err(::anyhow::anyhow!("Expected '{}' to be the int literal '{}'", #error_name, #literal));
} else {
v
}
},
| ::core::option::Option::Some(::convex::Value::Int64(value)) => {
let v = value.clone();
if v != #literal {
return Err(::anyhow::anyhow!("Expected '{}' to be the int literal '{}'", #error_name, #literal));
} else {
v
}
},
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be an int literal", #error_name));
},
};
}
}
fn print_extract_bool_literal(
ident: Ident,
match_target: TokenStream,
error_name: String,
literal: bool,
) -> TokenStream {
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Boolean(value)) => {
let v = value.clone();
if v != #literal {
return Err(::anyhow::anyhow!("Expected '{}' to be the boolean literal '{}'", #error_name, #literal));
} else {
v
}
},
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be a boolean literal", #error_name));
},
};
}
}
fn print_extract_string_literal(
ident: Ident,
match_target: TokenStream,
error_name: String,
literal: String,
) -> TokenStream {
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::String(value)) => {
let v = value.clone();
if v != #literal {
return Err(::anyhow::anyhow!("Expected '{}' to be the string literal '{}'", #error_name, #literal));
} else {
v
}
},
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be a string literal", #error_name));
},
};
}
}
fn print_extract_number(
ident: Ident,
match_target: TokenStream,
error_name: String,
) -> TokenStream {
// TODO: Should this work for both Ints and Floats?
quote! {
let #ident = match #match_target {
| ::core::option::Option::Some(::convex::Value::Float64(value)) => value.clone(),
| ::core::option::Option::Some(::convex::Value::Int64(value)) => value.clone() as f64,
| _ => {
return Err(::anyhow::anyhow!("Expected '{}' to be a number", #error_name));
},
};
}
}
fn parse_child(name: &ConvexName, input: ParseStream) -> Result<Self> {
// name: v.string(...)
// ^^^^
let ident = Ident::parse(input)?;
let id = ident.to_string();
let name = ConvexName { path: name.full_path(), id };
// name: v.string(...)
// ^
let _ = input.parse::<Token![:]>()?;
// name: v.string(...)
// ^^^^^^^^^^^^^
let t = Self::parse_validator_call(&name, input)?;
Ok(Self { name, t })
}
fn parse_validator_call(
name: &ConvexName,
input: ParseStream,
) -> Result<ConvexType> {
// v.string(...)
// ^
let v = Ident::parse(input)?;
if v != "v" {
return Err(Error::new_spanned(&v, "Expected v.method()"));
}
// v.string(...)
// ^
let _ = input.parse::<Token![.]>()?;
// v.string(...)
// ^^^^^^
let method_ident = Ident::parse(input)?;
let method = method_ident.to_string();
// v.string(...)
// ^^^^^
let inner;
let _ = syn::parenthesized!(inner in input);
match method.as_str() {
| "id" => {
let lit = Lit::parse(&inner)?;
match lit.clone() {
| Lit::Str(str_lit) => Ok(ConvexType::Id(str_lit.value())),
| _ => Err(Error::new_spanned(&lit, "Expected string literal")),
}
},
| "null" => Ok(ConvexType::Null),
| "int64" => Ok(ConvexType::Int64),
| "number" => Ok(ConvexType::Number),
| "boolean" => Ok(ConvexType::Bool),
| "string" => Ok(ConvexType::String),
| "literal" => {
let lit = Lit::parse(&inner)?;
match lit.clone() {
| Lit::Str(s) => Ok(ConvexType::StringLiteral(s.value())),
| Lit::Bool(b) => Ok(ConvexType::BoolLiteral(b.value())),
| Lit::Int(i) => Ok(ConvexType::IntLiteral(i.base10_parse::<i64>()?)),
| _ => Err(Error::new_spanned(&lit, "Unsupported literal")),
}
},
| "optional" => {
let t = Self::parse_validator_call(name, &inner)?;
Ok(ConvexType::Optional(Box::new(ConvexField {
name: name.clone(),
t,
})))
},
| "object" => {
let object_inner;
let _ = syn::braced!(object_inner in inner);
let ts =
Self::parse_comma_separated(name, &object_inner, |name, b| {
Self::parse_child(name, b)
})?;
Ok(ConvexType::Object(ts))
},
| "union" => {
let ts = Self::parse_comma_separated(name, &inner, |name, b| {
Self::parse_validator_call(name, b)
})?;
if ts.len() < 2 {
return Err(Error::new_spanned(
&method_ident,
"Unions must have 2 or more branches",
));
}
Ok(ConvexType::Union(ts))
},
| _ => {
Err(Error::new_spanned(&method_ident, "Unsupported validator call"))
},
}
}
fn parse_comma_separated<T>(
name: &ConvexName,
buffer: &ParseBuffer,
f: fn(&ConvexName, &ParseBuffer) -> Result<T>,
) -> Result<Vec<T>> {
let mut results = Vec::new();
let mut first = true;
let mut comma_token = Ok(());
while !buffer.is_empty() {
if !first {
// Must have comma token if this wasn't the first item.
comma_token?;
}
let x = f(name, buffer)?;
results.push(x);
comma_token = buffer.parse::<Token![,]>().map(|_| ());
first = false;
}
Ok(results)
}
}
fn capitalize_first_char(s: &str) -> String {
s.char_indices().fold(String::new(), |mut acc, (i, c)| {
if i == 0 {
acc.extend(c.to_uppercase());
} else {
acc.push(c);
}
acc
})
}

70
tests/main.rs Normal file
View file

@ -0,0 +1,70 @@
use convex::Value;
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() {
let convex_data = Value::Object(btreemap! {
"one".into() => Value::String("123fakeid".into()),
"two".into() => Value::Null,
"three".into() => Value::Int64(42),
"four".into() => Value::Float64(3.5),
"five".into() => Value::Boolean(true),
"six".into() => Value::String("Hello World".into()),
"seven".into() => Value::String("seven".into()),
"eight".into() => Value::Boolean(false),
"nine".into() => Value::Int64(9),
"ten".into() => Value::Float64(10.0),
});
let model =
Example::from_convex_value(&convex_data).expect("Model should parse data");
assert_eq!("123fakeid", model.one);
assert_eq!(42, model.three);
assert_eq!(3.5, model.four);
assert!(model.five);
assert_eq!("Hello World", model.six);
assert_eq!("seven", model.seven);
assert!(!model.eight);
assert_eq!(9, model.nine);
if let ExampleTen::Variant2(value) = model.ten {
assert_eq!(10.0, value);
} else {
panic!("Expected 10")
}
let expected_json_data = json!({
"one": "123fakeid",
"two": null,
"three": 42,
"four": 3.5,
"five": true,
"six": "Hello World",
"seven": "seven",
"eight": false,
"nine": 9,
// TODO: Fix this.
"ten": {
"Variant2": 10.0
},
});
let actual_json_data = json!(model);
assert_eq!(expected_json_data, actual_json_data);
}