restson
This commit is contained in:
commit
a0d3c6cf9c
8 changed files with 2415 additions and 0 deletions
1894
cli/Cargo.lock
generated
Normal file
1894
cli/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
cli/Cargo.toml
Normal file
18
cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "tb-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
bon = "3.9.0"
|
||||||
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
|
clap = { version = "4.5.60", features = ["cargo", "derive", "env" ] }
|
||||||
|
env_logger = "0.11.9"
|
||||||
|
http = "1.4.0"
|
||||||
|
restson = "1.5.0"
|
||||||
|
rpassword = "7.4.0"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.149"
|
||||||
|
tokio = { version = "1.49.0", features = ["macros", "rt-multi-thread"] }
|
||||||
|
url = "2.5.8"
|
||||||
78
cli/src/api/mod.rs
Normal file
78
cli/src/api/mod.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
//use bon::Builder;
|
||||||
|
//use chrono::{DateTime,Utc};
|
||||||
|
//use restson::{RestClient, RestPath, Error};
|
||||||
|
//
|
||||||
|
//pub enum Order {
|
||||||
|
// Ascending,
|
||||||
|
// Descending,
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//impl AsRef<str> for Order {
|
||||||
|
// fn as_ref(&self) -> &str {
|
||||||
|
// match self {
|
||||||
|
// Self::Ascending => "asc",
|
||||||
|
// Self::Descending => "desc",
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//impl From<bool> for Order {
|
||||||
|
// fn from(ascending: bool) -> Self {
|
||||||
|
// if ascending {
|
||||||
|
// Self::Ascending
|
||||||
|
// } else {
|
||||||
|
// Self::Descending
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct Spond {
|
||||||
|
id: String,
|
||||||
|
|
||||||
|
}
|
||||||
|
//
|
||||||
|
//#[bon::builder]
|
||||||
|
//#[builder(on(bool, default=false))]
|
||||||
|
//fn sponds(
|
||||||
|
// comments: bool,
|
||||||
|
// hidden: bool,
|
||||||
|
// add_profile_info: bool,
|
||||||
|
// scheduled=bool,
|
||||||
|
// #[builder(into)]
|
||||||
|
// order=Order,
|
||||||
|
// #[builder(default = 20)]
|
||||||
|
// max=usize,
|
||||||
|
// min_end_timestamp=Option<DateTime<Utc>>,
|
||||||
|
// max_end_timestamp=Option<DateTime<Utc>>,
|
||||||
|
//) -> Get<(), Vec<Spond>> {
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//struct Sponds(Query);
|
||||||
|
//
|
||||||
|
//async pub fn sponds(
|
||||||
|
// #[builder(finish_fn)]
|
||||||
|
// client: &restson::RestClient, E
|
||||||
|
//
|
||||||
|
//
|
||||||
|
|
||||||
|
crate::get!(search(), () => "sponds" -> Vec<Spond>);
|
||||||
|
|
||||||
|
//impl Search {
|
||||||
|
// with_comments(
|
||||||
|
//#[bon::builder]
|
||||||
|
//#[builder(on(bool, default=false))]
|
||||||
|
//fn sponds(
|
||||||
|
// comments: bool,
|
||||||
|
// hidden: bool,
|
||||||
|
// add_profile_info: bool,
|
||||||
|
// scheduled=bool,
|
||||||
|
// #[builder(into)]
|
||||||
|
// order=Order,
|
||||||
|
// #[builder(default = 20)]
|
||||||
|
// max=usize,
|
||||||
|
// min_end_timestamp=Option<DateTime<Utc>>,
|
||||||
|
// max_end_timestamp=Option<DateTime<Utc>>,
|
||||||
|
//) -> Search {
|
||||||
|
//}
|
||||||
165
cli/src/authentication.rs
Normal file
165
cli/src/authentication.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
use clap::{Args, ArgGroup};
|
||||||
|
use restson::{RestClient, RestPath, Response};
|
||||||
|
use anyhow::{Result, Error};
|
||||||
|
use serde::{
|
||||||
|
ser::{Serialize, Serializer, SerializeMap},
|
||||||
|
Deserialize,
|
||||||
|
};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
#[command(group(
|
||||||
|
ArgGroup::new("authentication")
|
||||||
|
.args(["access", "refresh", "email", "phone"])
|
||||||
|
))]
|
||||||
|
pub struct Authentication {
|
||||||
|
#[arg(long)]
|
||||||
|
access: Option<Token>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
refresh: Option<Token>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
email: Option<Email>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
phone: Option<Phone>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Authentication {
|
||||||
|
pub async fn apply(self, client: RestClient) -> Result<RestClient> {
|
||||||
|
let client = match (self.access, self.refresh, self.email, self.phone) {
|
||||||
|
(Some(v), None, None, None) => v.apply(client)?,
|
||||||
|
(None, Some(v), None, None) => Tokens::authenticate(client, v).await?,
|
||||||
|
(None, None, Some(v), None) => Tokens::authenticate(client, v).await?,
|
||||||
|
(None, None, None, Some(v)) => Tokens::authenticate(client, v).await?,
|
||||||
|
(None, None, None, None) => client,
|
||||||
|
(a, b, c, d) => anyhow::bail!("invalid authentication: {} + {} + {} + {}", a.is_some(), b.is_some(), c.is_some(), d.is_some()),
|
||||||
|
};
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod identifier {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Email;
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
trait Identifier: Clone {
|
||||||
|
const NAME: &'static str;
|
||||||
|
type Value: std::str::FromStr<Err=Self::Error> + std::fmt::Debug + Clone + serde::Serialize;
|
||||||
|
type Error: std::error::Error + Send + Sync + 'static;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Identifier for identifier::Email {
|
||||||
|
const NAME: &'static str = "email";
|
||||||
|
type Value = String;
|
||||||
|
type Error = <String as std::str::FromStr>::Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Identifier for identifier::Phone {
|
||||||
|
const NAME: &'static str = "phone";
|
||||||
|
type Value = String;
|
||||||
|
type Error = <String as std::str::FromStr>::Err;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct WithPassword<I: Identifier> {
|
||||||
|
value: I::Value,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Identifier> Serialize for WithPassword<I> {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
{
|
||||||
|
let mut map = serializer.serialize_map(Some(2))?;
|
||||||
|
map.serialize_entry(I::NAME, &self.value)?;
|
||||||
|
map.serialize_entry("password", &self.password)?;
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Identifier> RestPath<()> for WithPassword<I> {
|
||||||
|
fn get_path(_: ()) -> std::result::Result<String, restson::Error> {
|
||||||
|
Ok(String::from("auth2/login"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Email = WithPassword<identifier::Email>;
|
||||||
|
type Phone = WithPassword<identifier::Phone>;
|
||||||
|
|
||||||
|
impl<I: Identifier> std::str::FromStr for WithPassword<I> {
|
||||||
|
type Err= Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let password = match std::env::var("SPOND_PASSWORD") {
|
||||||
|
Ok(password) => password,
|
||||||
|
Err(_) => rpassword::prompt_password("Password: ")?,
|
||||||
|
};
|
||||||
|
let value = I::Value::from_str(s)?;
|
||||||
|
Ok(Self { value, password })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Tokens {
|
||||||
|
#[serde(rename = "accessToken")]
|
||||||
|
access: TokenWithExpiration,
|
||||||
|
#[serde(rename = "refreshToken")]
|
||||||
|
refresh: TokenWithExpiration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tokens {
|
||||||
|
async fn authenticate<R: serde::Serialize + RestPath<()>>(client: RestClient, request: R) -> Result<RestClient> {
|
||||||
|
let tokens: Response<Self> = client.post_capture((), &request).await?;
|
||||||
|
tokens.into_inner().apply(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(self, client: RestClient) -> Result<RestClient> {
|
||||||
|
println!("refresh: {self:?}");
|
||||||
|
self.access.token.apply(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct TokenWithExpiration {
|
||||||
|
token: Token,
|
||||||
|
#[allow(unused)]
|
||||||
|
expiration: DateTime<Utc>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct Token(String);
|
||||||
|
|
||||||
|
impl Serialize for Token {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
{
|
||||||
|
let mut map = serializer.serialize_map(Some(1))?;
|
||||||
|
map.serialize_entry("token", &self.0)?;
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Token {
|
||||||
|
fn apply(self, mut client: RestClient) -> Result<RestClient> {
|
||||||
|
client.set_header("Authorization", &format!("Bearer {}", self.0))?;
|
||||||
|
Ok(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RestPath<()> for Token {
|
||||||
|
fn get_path(_: ()) -> std::result::Result<String, restson::Error> {
|
||||||
|
Ok(String::from("auth2/login/refresh"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::str::FromStr for Token {
|
||||||
|
type Err = std::convert::Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(Self(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
62
cli/src/main.rs
Normal file
62
cli/src/main.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use restson::RestClient;
|
||||||
|
use anyhow::Result;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
mod authentication;
|
||||||
|
mod api;
|
||||||
|
|
||||||
|
mod request;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about)]
|
||||||
|
struct Cli {
|
||||||
|
#[command(flatten)]
|
||||||
|
authentication: authentication::Authentication,
|
||||||
|
|
||||||
|
#[arg(long, default_value = "https://api.spond.com/")]
|
||||||
|
base: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub async fn client(self) -> Result<RestClient> {
|
||||||
|
let base = self.base.join("/core/v1/")?;
|
||||||
|
let client = RestClient::new(base.as_str())?;
|
||||||
|
Ok(self.authentication.apply(client).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct Spond(serde_json::Value);
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct Sponds(Vec<Spond>);
|
||||||
|
|
||||||
|
impl restson::RestPath<()> for Sponds {
|
||||||
|
fn get_path(_: ()) -> std::result::Result<String, restson::Error> {
|
||||||
|
Ok(String::from("sponds"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
env_logger::init();
|
||||||
|
|
||||||
|
let client = Cli::parse().client().await?;
|
||||||
|
|
||||||
|
// https://api.spond.com/core/v1/sponds?includeComments=true&includeHidden=false&addProfileInfo=true&scheduled=true&order=asc&max=20&prevId=F94829E35A9B4A48A042646C8B658B01&minStartTimestamp=2026-04-11T09:45:00Z&minEndTimestamp=2026-02-26T23:00:00.001Z
|
||||||
|
let query = [
|
||||||
|
("includeComments", "true"),
|
||||||
|
("includeHidden", "false"),
|
||||||
|
("addProfileInfo", "true"),
|
||||||
|
("scheduled", "true"),
|
||||||
|
("order", "asc"),
|
||||||
|
("max", "20"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for spond in client.get_with::<_, Sponds>((), &query).await?.into_inner().0 {
|
||||||
|
println!("{spond:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
111
cli/src/request/get.rs
Normal file
111
cli/src/request/get.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/// GET macro
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get {
|
||||||
|
// Case 1: no query
|
||||||
|
(
|
||||||
|
$name:ident,
|
||||||
|
( $( $arg:ident : $arg_ty:ty ),* ) => $path:literal -> $out:ty $(,)?
|
||||||
|
) => {
|
||||||
|
get!($name(), ( $( $arg : $arg_ty ),* ) => $path -> $out);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Case 2: empty query ()
|
||||||
|
(
|
||||||
|
$name:ident (),
|
||||||
|
( $( $arg:ident : $arg_ty:ty ),* ) => $path:literal -> $out:ty $(,)?
|
||||||
|
) => {
|
||||||
|
#[bon::builder]
|
||||||
|
pub fn $name(
|
||||||
|
#[builder(finish_fn)] client: &restson::RestClient,
|
||||||
|
$( #[builder(finish_fn)] $arg: $arg_ty ),*
|
||||||
|
) -> impl std::future::Future<Output = Result<$out, restson::Error>> + '_ {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct RP($out);
|
||||||
|
|
||||||
|
impl restson::RestPath<( $( $arg_ty ),* )> for RP {
|
||||||
|
fn get_path(args: ( $( $arg_ty ),* )) -> Result<String, restson::Error> {
|
||||||
|
let ( $( $arg ),* ) = args;
|
||||||
|
Ok(format!($path, $( $arg = $arg ),* ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let result = client.get_with::<_, RP>(( $( $arg ),* ), &[]).await?;
|
||||||
|
Ok(result.into_inner().0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Case 3: query with flags / mixed types
|
||||||
|
(
|
||||||
|
$name:ident ( $( $query:tt ),* $(,)? ),
|
||||||
|
( $( $arg:ident : $arg_ty:ty ),* ) => $path:literal -> $out:ty $(,)?
|
||||||
|
) => {
|
||||||
|
#[bon::builder]
|
||||||
|
pub fn $name(
|
||||||
|
#[builder(finish_fn)] client: &restson::RestClient,
|
||||||
|
$( #[builder(finish_fn)] $arg: $arg_ty, )*
|
||||||
|
$(
|
||||||
|
get!(@query_field $query)
|
||||||
|
)*
|
||||||
|
) -> impl std::future::Future<Output = Result<$out, restson::Error>> + '_ {
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Query<'a> {
|
||||||
|
$(
|
||||||
|
get!(@query_field_struct $query)
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Query<'_> {
|
||||||
|
fn as_pairs(&self) -> Vec<(&str, String)> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
$(
|
||||||
|
get!(@push_pair out, self, $query)
|
||||||
|
)*
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = Query {
|
||||||
|
$(
|
||||||
|
get!(@query_field_init $query)
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct RP($out);
|
||||||
|
|
||||||
|
impl restson::RestPath<( $( $arg_ty ),* )> for RP {
|
||||||
|
fn get_path(args: ( $( $arg_ty ),* )) -> Result<String, restson::Error> {
|
||||||
|
let ( $( $arg ),* ) = args;
|
||||||
|
Ok(format!($path, $( $arg = $arg ),* ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let result = client.get_with::<_, RP>(&query.as_pairs(), ( $( $arg ),* )).await?;
|
||||||
|
Ok(result.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query helpers
|
||||||
|
(@query_field $field:ident) => { $field: Option<bool>, };
|
||||||
|
(@query_field $field:ident = $ty:ty) => { $field: $ty, };
|
||||||
|
|
||||||
|
(@query_field_struct $field:ident) => { $field: Option<bool>, };
|
||||||
|
(@query_field_struct $field:ident = $ty:ty) => { $field: $ty, };
|
||||||
|
|
||||||
|
(@query_field_init $field:ident) => { $field, };
|
||||||
|
(@query_field_init $field:ident = $ty:ty) => { $field, };
|
||||||
|
|
||||||
|
(@push_pair $vec:ident, $self:ident, $field:ident = $ty:ty) => {
|
||||||
|
$vec.push((stringify!($field), $self.$field.to_string()));
|
||||||
|
};
|
||||||
|
(@push_pair $vec:ident, $self:ident, $field:ident) => {
|
||||||
|
if let Some(v) = &$self.$field {
|
||||||
|
$vec.push((stringify!($field), v.to_string()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
2
cli/src/request/mod.rs
Normal file
2
cli/src/request/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod get;
|
||||||
|
pub mod post;
|
||||||
85
cli/src/request/post.rs
Normal file
85
cli/src/request/post.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/// Generate a POST function
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! post {
|
||||||
|
(
|
||||||
|
$name:ident ( $( $query:tt ),* $(,)? ),
|
||||||
|
$( $body:ident = $body_ty:ty ),* $(,)?,
|
||||||
|
( $( $arg:ident : $arg_ty:ty ),* ) => $path:expr $(,)?
|
||||||
|
) => {
|
||||||
|
#[bon::builder]
|
||||||
|
pub fn $name(
|
||||||
|
#[builder(finish_fn)]
|
||||||
|
client: &restson::Client,
|
||||||
|
$( #[builder(finish_fn)] $arg: $arg_ty, )*
|
||||||
|
$(
|
||||||
|
post!(@query_field $query)
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
$body: $body_ty,
|
||||||
|
)*
|
||||||
|
) -> impl std::future::Future<Output = Result<(), restson::Error>> + '_
|
||||||
|
{
|
||||||
|
// Query struct
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Query<'a> {
|
||||||
|
$(
|
||||||
|
post!(@query_field_struct $query)
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body struct
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Body<'a> {
|
||||||
|
$(
|
||||||
|
$body: $body_ty,
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = Query {
|
||||||
|
$(
|
||||||
|
post!(@query_field_init $query)
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = Body {
|
||||||
|
$(
|
||||||
|
$body,
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
|
||||||
|
impl restson::RestPath<( $( $arg_ty ),* )> for Body<'_> {
|
||||||
|
fn get_url($( $arg: $arg_ty ),*) -> Result<String, restson::Error> {
|
||||||
|
Ok(format!($path, $( $arg = $arg ),* ))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.post_capture_with::<_, _, _>(&body, &query, ( $( $arg ),* ))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: parse query field plain vs typed
|
||||||
|
(@query_field $field:ident) => {
|
||||||
|
$field: Option<bool>,
|
||||||
|
};
|
||||||
|
(@query_field $field:ident = $ty:ty) => {
|
||||||
|
$field: $ty,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: struct fields
|
||||||
|
(@query_field_struct $field:ident) => {
|
||||||
|
$field: Option<bool>,
|
||||||
|
};
|
||||||
|
(@query_field_struct $field:ident = $ty:ty) => {
|
||||||
|
$field: $ty,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: initialize query fields
|
||||||
|
(@query_field_init $field:ident) => {
|
||||||
|
$field,
|
||||||
|
};
|
||||||
|
(@query_field_init $field:ident = $ty:ty) => {
|
||||||
|
$field,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue