mirror of
https://github.com/danbulant/convex-macros
synced 2026-05-19 03:58:31 +00:00
chore: initial commit
This commit is contained in:
commit
0f65ecc1d3
16 changed files with 2748 additions and 0 deletions
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
4
.husky/commit-msg
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm -- commitlint --edit ${1}
|
||||||
4
.husky/pre-commit
Executable file
4
.husky/pre-commit
Executable file
|
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
pnpm lint-staged
|
||||||
4
.lintstagedrc.js
Normal file
4
.lintstagedrc.js
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = {
|
||||||
|
"*.rs": "rustfmt",
|
||||||
|
"*.{js,jsx,ts,tsx,json,yml,yaml,md}": "prettier --write",
|
||||||
|
};
|
||||||
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
v20.3.1
|
||||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
pnpm-lock.yaml
|
||||||
22
Cargo.toml
Normal file
22
Cargo.toml
Normal 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
21
LICENSE
Normal 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
5
README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
# convex-macros
|
||||||
|
|
||||||
|
[](https://github.com/ragkit/convex-macros/actions/workflows/ci.yml)
|
||||||
|
|
||||||
|
Macros to help make Convex in Rust nice
|
||||||
1
commitlint.config.js
Normal file
1
commitlint.config.js
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = { extends: ["@commitlint/config-conventional"] };
|
||||||
17
package.json
Normal file
17
package.json
Normal 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
1710
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
27
rustfmt.toml
Normal file
27
rustfmt.toml
Normal 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
15
src/lib.rs
Normal 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
815
src/model.rs
Normal 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
70
tests/main.rs
Normal 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);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue