feat(v2): initial commit
This commit is contained in:
parent
48868983b3
commit
5a84558d89
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -1,4 +1,12 @@
|
||||||
|
# rust
|
||||||
/target
|
/target
|
||||||
/Cargo.lock
|
|
||||||
.env
|
# direnv
|
||||||
|
.envrc
|
||||||
|
.direnv
|
||||||
|
|
||||||
|
# vim
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# mac os
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
1746
Cargo.lock
generated
Normal file
1746
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
11
Cargo.toml
11
Cargo.toml
|
@ -1,3 +1,12 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [ "torn-api-macros", "torn-api", "torn-key-pool" ]
|
members = ["torn-api", "torn-api-codegen"]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = { version = "1" }
|
||||||
|
syn = { version = "2" }
|
||||||
|
proc-macro2 = { version = "1" }
|
||||||
|
|
||||||
|
[profile.dev.package.torn-api-codegen]
|
||||||
|
opt-level = 3
|
||||||
|
|
114
flake.lock
Normal file
114
flake.lock
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"fenix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744618085,
|
||||||
|
"narHash": "sha256-+VdhZsIiIDtyOL88c4U/Os1PsCMLOCyScIeWL4hxJRM=",
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"rev": "a85d390a5607188dca2dbc39b5b37571651d69ce",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-community",
|
||||||
|
"repo": "fenix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744463964,
|
||||||
|
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744463964,
|
||||||
|
"narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"fenix": "fenix",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-analyzer-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1744539868,
|
||||||
|
"narHash": "sha256-NPUnfDAwLD69aKetxjC7lV5ysrvs1IKC0Sy4Zai10Mw=",
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"rev": "8365cf853e791c93fa8bc924f031f11949bb1a3c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "rust-lang",
|
||||||
|
"ref": "nightly",
|
||||||
|
"repo": "rust-analyzer",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
32
flake.nix
Normal file
32
flake.nix
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
description = "A very basic flake";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
|
fenix.url = "github:nix-community/fenix";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
fenix,
|
||||||
|
flake-utils,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs { inherit system; };
|
||||||
|
toolchain = fenix.packages.${system}.stable.toolchain;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
toolchain
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
15
torn-api-codegen/Cargo.toml
Normal file
15
torn-api-codegen/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "torn-api-codegen"
|
||||||
|
authors = ["Pyrit [2111649]"]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Contains the v2 torn API model descriptions and codegen for the bindings"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
heck = "0.5"
|
||||||
|
indexmap = { version = "2.9", features = ["serde"] }
|
||||||
|
quote = "1"
|
||||||
|
proc-macro2 = { workspace = true }
|
||||||
|
syn = { workspace = true }
|
14602
torn-api-codegen/openapi.json
Normal file
14602
torn-api-codegen/openapi.json
Normal file
File diff suppressed because it is too large
Load diff
0
torn-api-codegen/overrides.toml
Normal file
0
torn-api-codegen/overrides.toml
Normal file
2
torn-api-codegen/src/lib.rs
Normal file
2
torn-api-codegen/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod model;
|
||||||
|
pub mod openapi;
|
310
torn-api-codegen/src/model/enum.rs
Normal file
310
torn-api-codegen/src/model/enum.rs
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
use heck::ToUpperCamelCase;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
|
||||||
|
use crate::openapi::{
|
||||||
|
parameter::OpenApiParameterSchema,
|
||||||
|
r#type::{OpenApiType, OpenApiVariants},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum EnumRepr {
|
||||||
|
U8,
|
||||||
|
U32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum EnumVariantTupleValue {
|
||||||
|
Ref(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnumVariantTupleValue {
|
||||||
|
pub fn from_schema(schema: &OpenApiType) -> Option<Self> {
|
||||||
|
if let OpenApiType {
|
||||||
|
ref_path: Some(path),
|
||||||
|
..
|
||||||
|
} = schema
|
||||||
|
{
|
||||||
|
Some(Self::Ref((*path).to_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> Option<&str> {
|
||||||
|
let Self::Ref(path) = self;
|
||||||
|
|
||||||
|
path.strip_prefix("#/components/schemas/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum EnumVariantValue {
|
||||||
|
Repr(u32),
|
||||||
|
String { rename: Option<String> },
|
||||||
|
Tuple(Vec<EnumVariantTupleValue>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EnumVariantValue {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::String { rename: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnumVariantValue {
|
||||||
|
pub fn codegen_display(&self, name: &str) -> Option<TokenStream> {
|
||||||
|
match self {
|
||||||
|
Self::Repr(i) => Some(quote! { write!(f, "{}", #i) }),
|
||||||
|
Self::String { rename } => {
|
||||||
|
let name = rename.as_deref().unwrap_or(name);
|
||||||
|
Some(quote! { write!(f, #name) })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct EnumVariant {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub value: EnumVariantValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnumVariant {
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
let doc = self.description.as_ref().map(|d| {
|
||||||
|
quote! {
|
||||||
|
#[doc = #d]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
|
||||||
|
match &self.value {
|
||||||
|
EnumVariantValue::Repr(repr) => Some(quote! {
|
||||||
|
#doc
|
||||||
|
#name = #repr
|
||||||
|
}),
|
||||||
|
EnumVariantValue::String { rename } => {
|
||||||
|
let serde_attr = rename.as_ref().map(|r| {
|
||||||
|
quote! {
|
||||||
|
#[serde(rename = #r)]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#doc
|
||||||
|
#serde_attr
|
||||||
|
#name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
EnumVariantValue::Tuple(values) => {
|
||||||
|
let mut val_tys = Vec::with_capacity(values.len());
|
||||||
|
|
||||||
|
for value in values {
|
||||||
|
let ty_name = value.name()?;
|
||||||
|
let ty_name = format_ident!("{ty_name}");
|
||||||
|
|
||||||
|
val_tys.push(quote! {
|
||||||
|
crate::models::#ty_name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#name(#(#val_tys),*)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_display(&self) -> Option<TokenStream> {
|
||||||
|
let rhs = self.value.codegen_display(&self.name)?;
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
Self::#name => #rhs
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct Enum {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub repr: Option<EnumRepr>,
|
||||||
|
pub copy: bool,
|
||||||
|
pub display: bool,
|
||||||
|
pub untagged: bool,
|
||||||
|
pub variants: Vec<EnumVariant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Enum {
|
||||||
|
pub fn from_schema(name: &str, schema: &OpenApiType) -> Option<Self> {
|
||||||
|
let mut result = Enum {
|
||||||
|
name: name.to_owned(),
|
||||||
|
description: schema.description.as_deref().map(ToOwned::to_owned),
|
||||||
|
copy: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match &schema.r#enum {
|
||||||
|
Some(OpenApiVariants::Int(int_variants)) => {
|
||||||
|
result.repr = Some(EnumRepr::U32);
|
||||||
|
result.display = true;
|
||||||
|
result.variants = int_variants
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|i| EnumVariant {
|
||||||
|
name: format!("Variant{i}"),
|
||||||
|
value: EnumVariantValue::Repr(i as u32),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
Some(OpenApiVariants::Str(str_variants)) => {
|
||||||
|
result.display = true;
|
||||||
|
result.variants = str_variants
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|s| {
|
||||||
|
let transformed = s.replace('&', "And").to_upper_camel_case();
|
||||||
|
EnumVariant {
|
||||||
|
value: EnumVariantValue::String {
|
||||||
|
rename: (transformed != s).then(|| s.to_owned()),
|
||||||
|
},
|
||||||
|
name: transformed,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
None => return None,
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_parameter_schema(name: &str, schema: &OpenApiParameterSchema) -> Option<Self> {
|
||||||
|
let mut result = Self {
|
||||||
|
name: name.to_owned(),
|
||||||
|
copy: true,
|
||||||
|
display: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for var in schema.r#enum.as_ref()? {
|
||||||
|
let transformed = var.to_upper_camel_case();
|
||||||
|
result.variants.push(EnumVariant {
|
||||||
|
value: EnumVariantValue::String {
|
||||||
|
rename: (transformed != *var).then(|| transformed.clone()),
|
||||||
|
},
|
||||||
|
name: transformed,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_one_of(name: &str, schemas: &[OpenApiType]) -> Option<Self> {
|
||||||
|
let mut result = Self {
|
||||||
|
name: name.to_owned(),
|
||||||
|
untagged: true,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for schema in schemas {
|
||||||
|
let value = EnumVariantTupleValue::from_schema(schema)?;
|
||||||
|
let name = value.name()?.to_owned();
|
||||||
|
|
||||||
|
result.variants.push(EnumVariant {
|
||||||
|
name,
|
||||||
|
value: EnumVariantValue::Tuple(vec![value]),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
let repr = self.repr.map(|r| match r {
|
||||||
|
EnumRepr::U8 => quote! { #[repr(u8)]},
|
||||||
|
EnumRepr::U32 => quote! { #[repr(u32)]},
|
||||||
|
});
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
let desc = self.description.as_ref().map(|d| {
|
||||||
|
quote! {
|
||||||
|
#repr
|
||||||
|
#[doc = #d]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut display = Vec::with_capacity(self.variants.len());
|
||||||
|
let mut variants = Vec::with_capacity(self.variants.len());
|
||||||
|
for variant in &self.variants {
|
||||||
|
variants.push(variant.codegen()?);
|
||||||
|
|
||||||
|
if self.display {
|
||||||
|
display.push(variant.codegen_display()?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut derives = vec![];
|
||||||
|
|
||||||
|
if self.copy {
|
||||||
|
derives.extend_from_slice(&["Copy", "Hash"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let derives = derives.into_iter().map(|d| format_ident!("{d}"));
|
||||||
|
|
||||||
|
let serde_attr = self.untagged.then(|| {
|
||||||
|
quote! {
|
||||||
|
#[serde(untagged)]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let display = self.display.then(|| {
|
||||||
|
quote! {
|
||||||
|
impl std::fmt::Display for #name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
#(#display),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#desc
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Deserialize, #(#derives),*)]
|
||||||
|
#serde_attr
|
||||||
|
pub enum #name {
|
||||||
|
#(#variants),*
|
||||||
|
}
|
||||||
|
#display
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::openapi::schema::OpenApiSchema;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let revive_setting = schema.components.schemas.get("ReviveSetting").unwrap();
|
||||||
|
|
||||||
|
let r#enum = Enum::from_schema("ReviveSetting", revive_setting).unwrap();
|
||||||
|
|
||||||
|
let code = r#enum.codegen().unwrap();
|
||||||
|
|
||||||
|
panic!("{code}");
|
||||||
|
}
|
||||||
|
}
|
189
torn-api-codegen/src/model/mod.rs
Normal file
189
torn-api-codegen/src/model/mod.rs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
use r#enum::Enum;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use newtype::Newtype;
|
||||||
|
use object::Object;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
|
||||||
|
use crate::openapi::r#type::OpenApiType;
|
||||||
|
|
||||||
|
pub mod r#enum;
|
||||||
|
pub mod newtype;
|
||||||
|
pub mod object;
|
||||||
|
pub mod parameter;
|
||||||
|
pub mod path;
|
||||||
|
pub mod scope;
|
||||||
|
pub mod union;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Model {
|
||||||
|
Newtype(Newtype),
|
||||||
|
Enum(Enum),
|
||||||
|
Object(Object),
|
||||||
|
Unresolved,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve(r#type: &OpenApiType, name: &str, schemas: &IndexMap<&str, OpenApiType>) -> Model {
|
||||||
|
match r#type {
|
||||||
|
OpenApiType {
|
||||||
|
r#enum: Some(_), ..
|
||||||
|
} => Enum::from_schema(name, r#type).map_or(Model::Unresolved, Model::Enum),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("object"),
|
||||||
|
..
|
||||||
|
} => Object::from_schema_object(name, r#type, schemas)
|
||||||
|
.map_or(Model::Unresolved, Model::Object),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some(_), ..
|
||||||
|
} => Newtype::from_schema(name, r#type).map_or(Model::Unresolved, Model::Newtype),
|
||||||
|
OpenApiType {
|
||||||
|
one_of: Some(types),
|
||||||
|
..
|
||||||
|
} => Enum::from_one_of(name, types).map_or(Model::Unresolved, Model::Enum),
|
||||||
|
OpenApiType {
|
||||||
|
all_of: Some(types),
|
||||||
|
..
|
||||||
|
} => Object::from_all_of(name, types, schemas).map_or(Model::Unresolved, Model::Object),
|
||||||
|
_ => Model::Unresolved,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model {
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
match self {
|
||||||
|
Self::Newtype(newtype) => newtype.codegen(),
|
||||||
|
Self::Enum(r#enum) => r#enum.codegen(),
|
||||||
|
Self::Object(object) => object.codegen(),
|
||||||
|
Self::Unresolved => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::{
|
||||||
|
model::r#enum::{EnumRepr, EnumVariant},
|
||||||
|
openapi::schema::OpenApiSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_newtypes() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let user_id_schema = schema.components.schemas.get("UserId").unwrap();
|
||||||
|
|
||||||
|
let user_id = resolve(user_id_schema, "UserId", &schema.components.schemas);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
user_id,
|
||||||
|
Model::Newtype(Newtype {
|
||||||
|
name: "UserId".to_owned(),
|
||||||
|
description: None,
|
||||||
|
inner: newtype::NewtypeInner::I32,
|
||||||
|
copy: true,
|
||||||
|
ord: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let attack_code_schema = schema.components.schemas.get("AttackCode").unwrap();
|
||||||
|
|
||||||
|
let attack_code = resolve(attack_code_schema, "AttackCode", &schema.components.schemas);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
attack_code,
|
||||||
|
Model::Newtype(Newtype {
|
||||||
|
name: "AttackCode".to_owned(),
|
||||||
|
description: None,
|
||||||
|
inner: newtype::NewtypeInner::Str,
|
||||||
|
copy: false,
|
||||||
|
ord: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_enums() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let forum_feed_type_schema = schema.components.schemas.get("ForumFeedTypeEnum").unwrap();
|
||||||
|
|
||||||
|
let forum_feed_type = resolve(
|
||||||
|
forum_feed_type_schema,
|
||||||
|
"ForumFeedTypeEnum",
|
||||||
|
&schema.components.schemas,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(forum_feed_type, Model::Enum(Enum {
|
||||||
|
name: "ForumFeedType".to_owned(),
|
||||||
|
description: Some("This represents the type of the activity. Values range from 1 to 8 where:\n * 1 = 'X posted on a thread',\n * 2 = 'X created a thread',\n * 3 = 'X liked your thread',\n * 4 = 'X disliked your thread',\n * 5 = 'X liked your post',\n * 6 = 'X disliked your post',\n * 7 = 'X quoted your post'.".to_owned()),
|
||||||
|
repr: Some(EnumRepr::U32),
|
||||||
|
copy: true,
|
||||||
|
untagged: false,
|
||||||
|
display: true,
|
||||||
|
variants: vec![
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant1".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(1),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant2".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(2),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant3".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(3),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant4".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(4),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant5".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(5),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant6".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(6),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
EnumVariant {
|
||||||
|
name: "Variant7".to_owned(),
|
||||||
|
value: r#enum::EnumVariantValue::Repr(7),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_all() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut unresolved = vec![];
|
||||||
|
let total = schema.components.schemas.len();
|
||||||
|
|
||||||
|
for (name, desc) in &schema.components.schemas {
|
||||||
|
if resolve(desc, name, &schema.components.schemas) == Model::Unresolved {
|
||||||
|
unresolved.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to resolve {}/{} types. Could not resolve [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
total,
|
||||||
|
unresolved
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| format!("`{u}`"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
torn-api-codegen/src/model/newtype.rs
Normal file
144
torn-api-codegen/src/model/newtype.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
|
||||||
|
use crate::openapi::r#type::OpenApiType;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum NewtypeInner {
|
||||||
|
Str,
|
||||||
|
I32,
|
||||||
|
I64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Newtype {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub inner: NewtypeInner,
|
||||||
|
pub copy: bool,
|
||||||
|
pub ord: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Newtype {
|
||||||
|
pub fn from_schema(name: &str, schema: &OpenApiType) -> Option<Self> {
|
||||||
|
let name = name.to_owned();
|
||||||
|
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
match schema {
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("string"),
|
||||||
|
..
|
||||||
|
} => Some(Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
inner: NewtypeInner::Str,
|
||||||
|
copy: false,
|
||||||
|
ord: false,
|
||||||
|
}),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("integer"),
|
||||||
|
format: Some("int32"),
|
||||||
|
..
|
||||||
|
} => Some(Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
inner: NewtypeInner::I32,
|
||||||
|
copy: true,
|
||||||
|
ord: true,
|
||||||
|
}),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("integer"),
|
||||||
|
format: Some("int64"),
|
||||||
|
..
|
||||||
|
} => Some(Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
inner: NewtypeInner::I64,
|
||||||
|
copy: true,
|
||||||
|
ord: true,
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self) -> Option<proc_macro2::TokenStream> {
|
||||||
|
let mut derives = vec![quote! { Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize }];
|
||||||
|
|
||||||
|
if self.copy {
|
||||||
|
derives.push(quote! { Copy });
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.ord {
|
||||||
|
derives.push(quote! { PartialOrd, Ord });
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
let inner = match self.inner {
|
||||||
|
NewtypeInner::Str => format_ident!("String"),
|
||||||
|
NewtypeInner::I32 => format_ident!("i32"),
|
||||||
|
NewtypeInner::I64 => format_ident!("i64"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = self.description.as_ref().map(|d| {
|
||||||
|
quote! {
|
||||||
|
#[doc = #d]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = quote! {
|
||||||
|
#doc
|
||||||
|
#[derive(#(#derives),*)]
|
||||||
|
pub struct #name(pub #inner);
|
||||||
|
|
||||||
|
impl #name {
|
||||||
|
pub fn new(inner: #inner) -> Self {
|
||||||
|
Self(inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> #inner {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<#inner> for #name {
|
||||||
|
fn from(inner: #inner) -> Self {
|
||||||
|
Self(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<#name> for #inner {
|
||||||
|
fn from(outer: #name) -> Self {
|
||||||
|
outer.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for #name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::openapi::schema::OpenApiSchema;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let user_id = schema.components.schemas.get("UserId").unwrap();
|
||||||
|
|
||||||
|
let mut newtype = Newtype::from_schema("UserId", user_id).unwrap();
|
||||||
|
|
||||||
|
newtype.description = Some("Description goes here".to_owned());
|
||||||
|
|
||||||
|
let code = newtype.codegen().unwrap().to_string();
|
||||||
|
|
||||||
|
panic!("{code}");
|
||||||
|
}
|
||||||
|
}
|
448
torn-api-codegen/src/model/object.rs
Normal file
448
torn-api-codegen/src/model/object.rs
Normal file
|
@ -0,0 +1,448 @@
|
||||||
|
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{ToTokens, format_ident, quote};
|
||||||
|
use syn::Ident;
|
||||||
|
|
||||||
|
use crate::openapi::r#type::OpenApiType;
|
||||||
|
|
||||||
|
use super::r#enum::Enum;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PrimitiveType {
|
||||||
|
Bool,
|
||||||
|
I32,
|
||||||
|
I64,
|
||||||
|
String,
|
||||||
|
Float,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum PropertyType {
|
||||||
|
Primitive(PrimitiveType),
|
||||||
|
Ref(String),
|
||||||
|
Enum(Enum),
|
||||||
|
Nested(Box<Object>),
|
||||||
|
Array(Box<PropertyType>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PropertyType {
|
||||||
|
pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
|
||||||
|
match self {
|
||||||
|
Self::Primitive(PrimitiveType::Bool) => Some(format_ident!("bool").into_token_stream()),
|
||||||
|
Self::Primitive(PrimitiveType::I32) => Some(format_ident!("i32").into_token_stream()),
|
||||||
|
Self::Primitive(PrimitiveType::I64) => Some(format_ident!("i64").into_token_stream()),
|
||||||
|
Self::Primitive(PrimitiveType::String) => {
|
||||||
|
Some(format_ident!("String").into_token_stream())
|
||||||
|
}
|
||||||
|
Self::Primitive(PrimitiveType::Float) => Some(format_ident!("f64").into_token_stream()),
|
||||||
|
Self::Ref(path) => {
|
||||||
|
let name = path.strip_prefix("#/components/schemas/")?;
|
||||||
|
let name = format_ident!("{name}");
|
||||||
|
|
||||||
|
Some(quote! { crate::models::#name })
|
||||||
|
}
|
||||||
|
Self::Enum(r#enum) => {
|
||||||
|
let code = r#enum.codegen()?;
|
||||||
|
namespace.push_element(code);
|
||||||
|
|
||||||
|
let ns = namespace.get_ident();
|
||||||
|
let name = format_ident!("{}", r#enum.name);
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#ns::#name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Self::Array(array) => {
|
||||||
|
let inner_ty = array.codegen(namespace)?;
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
Vec<#inner_ty>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Self::Nested(nested) => {
|
||||||
|
let code = nested.codegen()?;
|
||||||
|
namespace.push_element(code);
|
||||||
|
|
||||||
|
let ns = namespace.get_ident();
|
||||||
|
let name = format_ident!("{}", nested.name);
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#ns::#name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Property {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub required: bool,
|
||||||
|
pub nullable: bool,
|
||||||
|
pub r#type: PropertyType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Property {
|
||||||
|
pub fn from_schema(
|
||||||
|
name: &str,
|
||||||
|
required: bool,
|
||||||
|
schema: &OpenApiType,
|
||||||
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let name = name.to_owned();
|
||||||
|
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
match schema {
|
||||||
|
OpenApiType {
|
||||||
|
r#enum: Some(_), ..
|
||||||
|
} => Some(Self {
|
||||||
|
r#type: PropertyType::Enum(Enum::from_schema(
|
||||||
|
&name.clone().to_upper_camel_case(),
|
||||||
|
schema,
|
||||||
|
)?),
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
}),
|
||||||
|
OpenApiType {
|
||||||
|
one_of: Some(types),
|
||||||
|
..
|
||||||
|
} => match types.as_slice() {
|
||||||
|
[
|
||||||
|
left,
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("null"),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
] => {
|
||||||
|
let mut inner = Self::from_schema(&name, required, left, schemas)?;
|
||||||
|
inner.nullable = true;
|
||||||
|
Some(inner)
|
||||||
|
}
|
||||||
|
[
|
||||||
|
left @ ..,
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("null"),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
] => {
|
||||||
|
let rest = OpenApiType {
|
||||||
|
one_of: Some(left.to_owned()),
|
||||||
|
..schema.clone()
|
||||||
|
};
|
||||||
|
let mut inner = Self::from_schema(&name, required, &rest, schemas)?;
|
||||||
|
inner.nullable = true;
|
||||||
|
Some(inner)
|
||||||
|
}
|
||||||
|
cases => {
|
||||||
|
let r#enum = Enum::from_one_of(&name.to_upper_camel_case(), cases)?;
|
||||||
|
Some(Self {
|
||||||
|
name,
|
||||||
|
description: None,
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
r#type: PropertyType::Enum(r#enum),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
OpenApiType {
|
||||||
|
all_of: Some(types),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
|
||||||
|
Some(Self {
|
||||||
|
name,
|
||||||
|
description: None,
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
r#type: PropertyType::Nested(Box::new(composite)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("object"),
|
||||||
|
..
|
||||||
|
} => Some(Self {
|
||||||
|
r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
|
||||||
|
&name.clone().to_upper_camel_case(),
|
||||||
|
schema,
|
||||||
|
schemas,
|
||||||
|
)?)),
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
}),
|
||||||
|
OpenApiType {
|
||||||
|
ref_path: Some(path),
|
||||||
|
..
|
||||||
|
} => Some(Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
r#type: PropertyType::Ref((*path).to_owned()),
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
}),
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some("array"),
|
||||||
|
items: Some(items),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let inner = Self::from_schema(&name, required, items, schemas)?;
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
r#type: PropertyType::Array(Box::new(inner.r#type)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
OpenApiType {
|
||||||
|
r#type: Some(_), ..
|
||||||
|
} => {
|
||||||
|
let prim = match (schema.r#type, schema.format) {
|
||||||
|
(Some("integer"), Some("int32")) => PrimitiveType::I32,
|
||||||
|
(Some("integer"), Some("int64")) => PrimitiveType::I64,
|
||||||
|
(Some("number"), Some("float")) => PrimitiveType::Float,
|
||||||
|
(Some("string"), None) => PrimitiveType::String,
|
||||||
|
(Some("boolean"), None) => PrimitiveType::Bool,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
required,
|
||||||
|
nullable: false,
|
||||||
|
r#type: PropertyType::Primitive(prim),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self, namespace: &mut ObjectNamespace) -> Option<TokenStream> {
|
||||||
|
let desc = self.description.as_ref().map(|d| quote! { #[doc = #d]});
|
||||||
|
|
||||||
|
let name = &self.name;
|
||||||
|
let (name, serde_attr) = match name.as_str() {
|
||||||
|
"type" => (format_ident!("r#type"), None),
|
||||||
|
name if name != name.to_snake_case() => (
|
||||||
|
format_ident!("{}", name.to_snake_case()),
|
||||||
|
Some(quote! { #[serde(rename = #name)]}),
|
||||||
|
),
|
||||||
|
_ => (format_ident!("{name}"), None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ty_inner = self.r#type.codegen(namespace)?;
|
||||||
|
|
||||||
|
let ty = if !self.required || self.nullable {
|
||||||
|
quote! { Option<#ty_inner> }
|
||||||
|
} else {
|
||||||
|
ty_inner
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#desc
|
||||||
|
#serde_attr
|
||||||
|
pub #name: #ty
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub struct Object {
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub properties: Vec<Property>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Object {
|
||||||
|
pub fn from_schema_object(
|
||||||
|
name: &str,
|
||||||
|
schema: &OpenApiType,
|
||||||
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let mut result = Object {
|
||||||
|
name: name.to_owned(),
|
||||||
|
description: schema.description.as_deref().map(ToOwned::to_owned),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(props) = &schema.properties else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let required = schema.required.clone().unwrap_or_default();
|
||||||
|
|
||||||
|
for (prop_name, prop) in props {
|
||||||
|
// HACK: This will cause a duplicate key otherwise
|
||||||
|
if *prop_name == "itemDetails" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement custom enum for this (depends on overrides being added)
|
||||||
|
if *prop_name == "value" && name == "TornHof" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.properties.push(Property::from_schema(
|
||||||
|
prop_name,
|
||||||
|
required.contains(prop_name),
|
||||||
|
prop,
|
||||||
|
schemas,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_all_of(
|
||||||
|
name: &str,
|
||||||
|
types: &[OpenApiType],
|
||||||
|
schemas: &IndexMap<&str, OpenApiType>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let mut result = Self {
|
||||||
|
name: name.to_owned(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
for r#type in types {
|
||||||
|
let r#type = if let OpenApiType {
|
||||||
|
ref_path: Some(path),
|
||||||
|
..
|
||||||
|
} = r#type
|
||||||
|
{
|
||||||
|
let name = path.strip_prefix("#/components/schemas/")?;
|
||||||
|
schemas.get(name)?
|
||||||
|
} else {
|
||||||
|
r#type
|
||||||
|
};
|
||||||
|
let obj = Self::from_schema_object(name, r#type, schemas)?;
|
||||||
|
|
||||||
|
result.description = result.description.or(obj.description);
|
||||||
|
result.properties.extend(obj.properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
let doc = self.description.as_ref().map(|d| {
|
||||||
|
quote! {
|
||||||
|
#[doc = #d]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut namespace = ObjectNamespace {
|
||||||
|
object: self,
|
||||||
|
ident: None,
|
||||||
|
elements: Vec::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut props = Vec::with_capacity(self.properties.len());
|
||||||
|
for prop in &self.properties {
|
||||||
|
props.push(prop.codegen(&mut namespace)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
let ns = namespace.codegen();
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#ns
|
||||||
|
|
||||||
|
#doc
|
||||||
|
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
|
||||||
|
pub struct #name {
|
||||||
|
#(#props),*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ObjectNamespace<'o> {
|
||||||
|
object: &'o Object,
|
||||||
|
ident: Option<Ident>,
|
||||||
|
elements: Vec<TokenStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectNamespace<'_> {
|
||||||
|
pub fn get_ident(&mut self) -> Ident {
|
||||||
|
self.ident
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
let name = self.object.name.to_snake_case();
|
||||||
|
format_ident!("{name}")
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_element(&mut self, el: TokenStream) {
|
||||||
|
self.elements.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(mut self) -> Option<TokenStream> {
|
||||||
|
if self.elements.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let ident = self.get_ident();
|
||||||
|
let elements = self.elements;
|
||||||
|
Some(quote! {
|
||||||
|
pub mod #ident {
|
||||||
|
#(#elements)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::openapi::schema::OpenApiSchema;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_object() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let attack = schema.components.schemas.get("FactionUpgrades").unwrap();
|
||||||
|
|
||||||
|
let resolved =
|
||||||
|
Object::from_schema_object("FactionUpgrades", attack, &schema.components.schemas)
|
||||||
|
.unwrap();
|
||||||
|
let _code = resolved.codegen().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_objects() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut objects = 0;
|
||||||
|
let mut unresolved = vec![];
|
||||||
|
|
||||||
|
for (name, desc) in &schema.components.schemas {
|
||||||
|
if desc.r#type == Some("object") {
|
||||||
|
objects += 1;
|
||||||
|
if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
|
||||||
|
unresolved.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to resolve {}/{} objects. Could not resolve [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
objects,
|
||||||
|
unresolved
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| format!("`{u}`"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
431
torn-api-codegen/src/model/parameter.rs
Normal file
431
torn-api-codegen/src/model/parameter.rs
Normal file
|
@ -0,0 +1,431 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use heck::ToUpperCamelCase;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{ToTokens, format_ident, quote};
|
||||||
|
|
||||||
|
use crate::openapi::parameter::{
|
||||||
|
OpenApiParameter, OpenApiParameterDefault, OpenApiParameterSchema,
|
||||||
|
ParameterLocation as SchemaLocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::r#enum::Enum;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ParameterOptions<P> {
|
||||||
|
pub default: Option<P>,
|
||||||
|
pub minimum: Option<P>,
|
||||||
|
pub maximum: Option<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ParameterType {
|
||||||
|
I32 {
|
||||||
|
options: ParameterOptions<i32>,
|
||||||
|
},
|
||||||
|
String,
|
||||||
|
Boolean,
|
||||||
|
Enum {
|
||||||
|
options: ParameterOptions<String>,
|
||||||
|
r#type: Enum,
|
||||||
|
},
|
||||||
|
Schema {
|
||||||
|
type_name: String,
|
||||||
|
},
|
||||||
|
Array {
|
||||||
|
items: Box<ParameterType>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParameterType {
|
||||||
|
pub fn from_schema(name: &str, schema: &OpenApiParameterSchema) -> Option<Self> {
|
||||||
|
match schema {
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
r#type: Some("integer"),
|
||||||
|
// BUG: missing for some types in the spec
|
||||||
|
|
||||||
|
// format: Some("int32"),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let default = match schema.default {
|
||||||
|
Some(OpenApiParameterDefault::Int(d)) => Some(d),
|
||||||
|
None => None,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self::I32 {
|
||||||
|
options: ParameterOptions {
|
||||||
|
default,
|
||||||
|
minimum: schema.minimum,
|
||||||
|
maximum: schema.maximum,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
r#type: Some("string"),
|
||||||
|
r#enum: Some(variants),
|
||||||
|
..
|
||||||
|
} if variants.as_slice() == ["true", "false"]
|
||||||
|
|| variants.as_slice() == ["false", "true "] =>
|
||||||
|
{
|
||||||
|
Some(ParameterType::Boolean)
|
||||||
|
}
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
r#type: Some("string"),
|
||||||
|
r#enum: Some(_),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let default = match schema.default {
|
||||||
|
Some(OpenApiParameterDefault::Str(d)) => Some(d.to_owned()),
|
||||||
|
None => None,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(ParameterType::Enum {
|
||||||
|
options: ParameterOptions {
|
||||||
|
default,
|
||||||
|
minimum: None,
|
||||||
|
maximum: None,
|
||||||
|
},
|
||||||
|
r#type: Enum::from_parameter_schema(name, schema)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
r#type: Some("string"),
|
||||||
|
..
|
||||||
|
} => Some(ParameterType::String),
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
ref_path: Some(path),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let type_name = path.strip_prefix("#/components/schemas/")?.to_owned();
|
||||||
|
|
||||||
|
Some(ParameterType::Schema { type_name })
|
||||||
|
}
|
||||||
|
OpenApiParameterSchema {
|
||||||
|
r#type: Some("array"),
|
||||||
|
items: Some(items),
|
||||||
|
..
|
||||||
|
} => Some(Self::Array {
|
||||||
|
items: Box::new(Self::from_schema(name, items)?),
|
||||||
|
}),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_type_name(&self, name: &str) -> TokenStream {
|
||||||
|
match self {
|
||||||
|
Self::I32 { .. } | Self::String | Self::Enum { .. } | Self::Array { .. } => {
|
||||||
|
format_ident!("{name}").into_token_stream()
|
||||||
|
}
|
||||||
|
Self::Boolean => quote! { bool },
|
||||||
|
Self::Schema { type_name } => {
|
||||||
|
let type_name = format_ident!("{type_name}",);
|
||||||
|
quote! { crate::models::#type_name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ParameterLocation {
|
||||||
|
Query,
|
||||||
|
Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Parameter {
|
||||||
|
pub name: String,
|
||||||
|
pub value: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub r#type: ParameterType,
|
||||||
|
pub required: bool,
|
||||||
|
pub location: ParameterLocation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parameter {
|
||||||
|
pub fn from_schema(name: &str, schema: &OpenApiParameter) -> Option<Self> {
|
||||||
|
let name = match name {
|
||||||
|
"From" => "FromTimestamp".to_owned(),
|
||||||
|
"To" => "ToTimestamp".to_owned(),
|
||||||
|
name => name.to_owned(),
|
||||||
|
};
|
||||||
|
let value = schema.name.to_owned();
|
||||||
|
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
let location = match &schema.r#in {
|
||||||
|
SchemaLocation::Query => ParameterLocation::Query,
|
||||||
|
SchemaLocation::Path => ParameterLocation::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
let r#type = ParameterType::from_schema(&name, &schema.schema)?;
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
r#type,
|
||||||
|
required: schema.required,
|
||||||
|
location,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
match &self.r#type {
|
||||||
|
ParameterType::I32 { options } => {
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
|
||||||
|
let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
|
||||||
|
|
||||||
|
if options.default.is_some()
|
||||||
|
|| options.minimum.is_some()
|
||||||
|
|| options.maximum.is_some()
|
||||||
|
{
|
||||||
|
_ = writeln!(desc, "\n # Notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
let constructor = if let (Some(min), Some(max)) = (options.minimum, options.maximum)
|
||||||
|
{
|
||||||
|
_ = write!(desc, "Values have to lie between {min} and {max}. ");
|
||||||
|
let name_raw = &self.name;
|
||||||
|
quote! {
|
||||||
|
impl #name {
|
||||||
|
pub fn new(inner: i32) -> Result<Self, crate::ParameterError> {
|
||||||
|
if inner > #max || inner < #min {
|
||||||
|
Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
|
||||||
|
} else {
|
||||||
|
Ok(Self(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<i32> for #name {
|
||||||
|
type Error = crate::ParameterError;
|
||||||
|
fn try_from(inner: i32) -> Result<Self, Self::Error> {
|
||||||
|
if inner > #max || inner < #min {
|
||||||
|
Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
|
||||||
|
} else {
|
||||||
|
Ok(Self(inner))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
impl #name {
|
||||||
|
pub fn new(inner: i32) -> Self {
|
||||||
|
Self(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(default) = options.default {
|
||||||
|
_ = write!(desc, "The default value is {default}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = quote! {
|
||||||
|
#[doc = #desc]
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#doc
|
||||||
|
pub struct #name(i32);
|
||||||
|
|
||||||
|
#constructor
|
||||||
|
|
||||||
|
impl From<#name> for i32 {
|
||||||
|
fn from(value: #name) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #name {
|
||||||
|
pub fn into_inner(self) -> i32 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for #name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ParameterType::Enum { options, r#type } => {
|
||||||
|
let mut desc = self.description.as_deref().unwrap_or_default().to_owned();
|
||||||
|
if let Some(default) = &options.default {
|
||||||
|
let default = default.to_upper_camel_case();
|
||||||
|
_ = write!(
|
||||||
|
desc,
|
||||||
|
r#"
|
||||||
|
# Notes
|
||||||
|
The default value [Self::{}](self::{}#variant.{})"#,
|
||||||
|
default, self.name, default
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doc = quote! { #[doc = #desc]};
|
||||||
|
let inner = r#type.codegen()?;
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#doc
|
||||||
|
#inner
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ParameterType::Array { items } => {
|
||||||
|
let (inner_name, outer_name) = match items.as_ref() {
|
||||||
|
ParameterType::I32 { .. }
|
||||||
|
| ParameterType::String
|
||||||
|
| ParameterType::Array { .. }
|
||||||
|
| ParameterType::Enum { .. } => self.name.strip_suffix('s').map_or_else(
|
||||||
|
|| (self.name.to_owned(), format!("{}s", self.name)),
|
||||||
|
|s| (s.to_owned(), self.name.to_owned()),
|
||||||
|
),
|
||||||
|
ParameterType::Boolean => ("bool".to_owned(), self.name.clone()),
|
||||||
|
ParameterType::Schema { type_name } => (type_name.clone(), self.name.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let inner = Self {
|
||||||
|
r#type: *items.clone(),
|
||||||
|
name: inner_name.clone(),
|
||||||
|
..self.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut code = inner.codegen().unwrap_or_default();
|
||||||
|
|
||||||
|
let name = format_ident!("{}", outer_name);
|
||||||
|
let inner_ty = items.codegen_type_name(&inner_name);
|
||||||
|
|
||||||
|
code.extend(quote! {
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct #name(pub Vec<#inner_ty>);
|
||||||
|
|
||||||
|
impl std::fmt::Display for #name {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
let mut first = true;
|
||||||
|
for el in &self.0 {
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
write!(f, "{el}")?;
|
||||||
|
} else {
|
||||||
|
write!(f, ",{el}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(code)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::openapi::{path::OpenApiPathParameter, schema::OpenApiSchema};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_components() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut parameters = 0;
|
||||||
|
let mut unresolved = vec![];
|
||||||
|
|
||||||
|
for (name, desc) in &schema.components.parameters {
|
||||||
|
parameters += 1;
|
||||||
|
if Parameter::from_schema(name, desc).is_none() {
|
||||||
|
unresolved.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to resolve {}/{} params. Could not resolve [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
parameters,
|
||||||
|
unresolved
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| format!("`{u}`"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_inline() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut params = 0;
|
||||||
|
let mut unresolved = Vec::new();
|
||||||
|
|
||||||
|
for (path, body) in &schema.paths {
|
||||||
|
for param in &body.get.parameters {
|
||||||
|
if let OpenApiPathParameter::Inline(inline) = param {
|
||||||
|
params += 1;
|
||||||
|
if Parameter::from_schema(inline.name, inline).is_none() {
|
||||||
|
unresolved.push(format!("`{}.{}`", path, inline.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to resolve {}/{} inline params. Could not resolve [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
params,
|
||||||
|
unresolved.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_inline() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut params = 0;
|
||||||
|
let mut unresolved = Vec::new();
|
||||||
|
|
||||||
|
for (path, body) in &schema.paths {
|
||||||
|
for param in &body.get.parameters {
|
||||||
|
if let OpenApiPathParameter::Inline(inline) = param {
|
||||||
|
if inline.r#in == SchemaLocation::Query {
|
||||||
|
let Some(param) = Parameter::from_schema(inline.name, inline) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if matches!(
|
||||||
|
param.r#type,
|
||||||
|
ParameterType::Schema { .. }
|
||||||
|
| ParameterType::Boolean
|
||||||
|
| ParameterType::String
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
params += 1;
|
||||||
|
if param.codegen().is_none() {
|
||||||
|
unresolved.push(format!("`{}.{}`", path, inline.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to codegen {}/{} inline params. Could not codegen [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
params,
|
||||||
|
unresolved.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
482
torn-api-codegen/src/model/path.rs
Normal file
482
torn-api-codegen/src/model/path.rs
Normal file
|
@ -0,0 +1,482 @@
|
||||||
|
use std::{fmt::Write, ops::Deref};
|
||||||
|
|
||||||
|
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::Ident;
|
||||||
|
|
||||||
|
use crate::openapi::{
|
||||||
|
parameter::OpenApiParameter,
|
||||||
|
path::{OpenApiPath, OpenApiPathParameter, OpenApiResponseBody},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
parameter::{Parameter, ParameterLocation, ParameterType},
|
||||||
|
union::Union,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PathSegment {
|
||||||
|
Constant(String),
|
||||||
|
Parameter { name: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PathParameter {
|
||||||
|
Inline(Parameter),
|
||||||
|
Component(Parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum PathResponse {
|
||||||
|
Component { name: String },
|
||||||
|
// TODO: needs to be implemented
|
||||||
|
ArbitraryUnion(Union),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Path {
|
||||||
|
pub segments: Vec<PathSegment>,
|
||||||
|
pub name: String,
|
||||||
|
pub summary: Option<String>,
|
||||||
|
pub description: String,
|
||||||
|
pub parameters: Vec<PathParameter>,
|
||||||
|
pub response: PathResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
pub fn from_schema(
|
||||||
|
path: &str,
|
||||||
|
schema: &OpenApiPath,
|
||||||
|
parameters: &IndexMap<&str, OpenApiParameter>,
|
||||||
|
) -> Option<Self> {
|
||||||
|
let mut segments = Vec::new();
|
||||||
|
for segment in path.strip_prefix('/')?.split('/') {
|
||||||
|
if segment.starts_with('{') && segment.ends_with('}') {
|
||||||
|
segments.push(PathSegment::Parameter {
|
||||||
|
name: segment[1..(segment.len() - 1)].to_owned(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
segments.push(PathSegment::Constant(segment.to_owned()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let summary = schema.get.summary.as_deref().map(ToOwned::to_owned);
|
||||||
|
let description = schema.get.description.deref().to_owned();
|
||||||
|
|
||||||
|
let mut params = Vec::with_capacity(schema.get.parameters.len());
|
||||||
|
for parameter in &schema.get.parameters {
|
||||||
|
match ¶meter {
|
||||||
|
OpenApiPathParameter::Link { ref_path } => {
|
||||||
|
let name = ref_path
|
||||||
|
.strip_prefix("#/components/parameters/")?
|
||||||
|
.to_owned();
|
||||||
|
let param = parameters.get(&name.as_str())?;
|
||||||
|
params.push(PathParameter::Component(Parameter::from_schema(
|
||||||
|
&name, param,
|
||||||
|
)?));
|
||||||
|
}
|
||||||
|
OpenApiPathParameter::Inline(schema) => {
|
||||||
|
let name = schema.name.to_upper_camel_case();
|
||||||
|
let parameter = Parameter::from_schema(&name, schema)?;
|
||||||
|
params.push(PathParameter::Inline(parameter));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut suffixes = vec![];
|
||||||
|
let mut name = String::new();
|
||||||
|
|
||||||
|
for seg in &segments {
|
||||||
|
match seg {
|
||||||
|
PathSegment::Constant(val) => {
|
||||||
|
name.push_str(&val.to_upper_camel_case());
|
||||||
|
}
|
||||||
|
PathSegment::Parameter { name } => {
|
||||||
|
suffixes.push(format!("For{}", name.to_upper_camel_case()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for suffix in suffixes {
|
||||||
|
name.push_str(&suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = match &schema.get.response_content {
|
||||||
|
OpenApiResponseBody::Schema(link) => PathResponse::Component {
|
||||||
|
name: link
|
||||||
|
.ref_path
|
||||||
|
.strip_prefix("#/components/schemas/")?
|
||||||
|
.to_owned(),
|
||||||
|
},
|
||||||
|
OpenApiResponseBody::Union { any_of: _ } => PathResponse::ArbitraryUnion(
|
||||||
|
Union::from_schema("Response", &schema.get.response_content)?,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
segments,
|
||||||
|
name,
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
parameters: params,
|
||||||
|
response,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_request(&self) -> Option<TokenStream> {
|
||||||
|
let name = if self.segments.len() == 1 {
|
||||||
|
let Some(PathSegment::Constant(first)) = self.segments.first() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
format_ident!("{}Request", first.to_upper_camel_case())
|
||||||
|
} else {
|
||||||
|
format_ident!("{}Request", self.name)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ns = PathNamespace {
|
||||||
|
path: self,
|
||||||
|
ident: None,
|
||||||
|
elements: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fields = Vec::with_capacity(self.parameters.len());
|
||||||
|
let mut convert_field = Vec::with_capacity(self.parameters.len());
|
||||||
|
let mut start_fields = Vec::new();
|
||||||
|
let mut discriminant = Vec::new();
|
||||||
|
let mut discriminant_val = Vec::new();
|
||||||
|
let mut fmt_val = Vec::new();
|
||||||
|
|
||||||
|
for param in &self.parameters {
|
||||||
|
let (is_inline, param) = match ¶m {
|
||||||
|
PathParameter::Inline(param) => (true, param),
|
||||||
|
PathParameter::Component(param) => (false, param),
|
||||||
|
};
|
||||||
|
|
||||||
|
let ty = match ¶m.r#type {
|
||||||
|
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
||||||
|
let ty_name = format_ident!("{}", param.name);
|
||||||
|
|
||||||
|
if is_inline {
|
||||||
|
ns.push_element(param.codegen()?);
|
||||||
|
let path = ns.get_ident();
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#path::#ty_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
crate::parameters::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParameterType::String => quote! { String },
|
||||||
|
ParameterType::Boolean => quote! { bool },
|
||||||
|
ParameterType::Schema { type_name } => {
|
||||||
|
let ty_name = format_ident!("{}", type_name);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::models::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParameterType::Array { .. } => {
|
||||||
|
ns.push_element(param.codegen()?);
|
||||||
|
let ty_name = param.r#type.codegen_type_name(¶m.name);
|
||||||
|
let path = ns.get_ident();
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#path::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let name = format_ident!("{}", param.name.to_snake_case());
|
||||||
|
let query_val = ¶m.value;
|
||||||
|
|
||||||
|
if param.location == ParameterLocation::Path {
|
||||||
|
discriminant.push(ty.clone());
|
||||||
|
discriminant_val.push(quote! { self.#name });
|
||||||
|
let path_name = format_ident!("{}", param.value);
|
||||||
|
start_fields.push(quote! {
|
||||||
|
#[builder(start_fn)]
|
||||||
|
pub #name: #ty
|
||||||
|
});
|
||||||
|
fmt_val.push(quote! {
|
||||||
|
#path_name=self.#name
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let ty = if param.required {
|
||||||
|
convert_field.push(quote! {
|
||||||
|
.chain(std::iter::once(&self.#name).map(|v| (#query_val, v.to_string())))
|
||||||
|
});
|
||||||
|
ty
|
||||||
|
} else {
|
||||||
|
convert_field.push(quote! {
|
||||||
|
.chain(self.#name.as_ref().into_iter().map(|v| (#query_val, v.to_string())))
|
||||||
|
});
|
||||||
|
quote! { Option<#ty>}
|
||||||
|
};
|
||||||
|
|
||||||
|
fields.push(quote! {
|
||||||
|
pub #name: #ty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_ty = match &self.response {
|
||||||
|
PathResponse::Component { name } => {
|
||||||
|
let name = format_ident!("{name}");
|
||||||
|
quote! {
|
||||||
|
crate::models::#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathResponse::ArbitraryUnion(union) => {
|
||||||
|
let path = ns.get_ident();
|
||||||
|
let ty_name = format_ident!("{}", union.name);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#path::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut path_fmt_str = String::new();
|
||||||
|
for seg in &self.segments {
|
||||||
|
match seg {
|
||||||
|
PathSegment::Constant(val) => _ = write!(path_fmt_str, "/{}", val),
|
||||||
|
PathSegment::Parameter { name } => _ = write!(path_fmt_str, "/{{{}}}", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let PathResponse::ArbitraryUnion(union) = &self.response {
|
||||||
|
ns.push_element(union.codegen()?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ns = ns.codegen();
|
||||||
|
|
||||||
|
start_fields.extend(fields);
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#ns
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, bon::Builder)]
|
||||||
|
#[builder(state_mod(vis = "pub(crate)"))]
|
||||||
|
pub struct #name {
|
||||||
|
#(#start_fields),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::request::IntoRequest for #name {
|
||||||
|
#[allow(unused_parens)]
|
||||||
|
type Discriminant = (#(#discriminant),*);
|
||||||
|
type Response = #response_ty;
|
||||||
|
fn into_request(self) -> crate::request::ApiRequest<Self::Discriminant> {
|
||||||
|
#[allow(unused_parens)]
|
||||||
|
crate::request::ApiRequest {
|
||||||
|
path: format!(#path_fmt_str, #(#fmt_val),*),
|
||||||
|
parameters: std::iter::empty()
|
||||||
|
#(#convert_field)*
|
||||||
|
.collect(),
|
||||||
|
disriminant: (#(#discriminant_val),*),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen_scope_call(&self) -> Option<TokenStream> {
|
||||||
|
let mut extra_args = Vec::new();
|
||||||
|
let mut disc = Vec::new();
|
||||||
|
|
||||||
|
let snake_name = self.name.to_snake_case();
|
||||||
|
|
||||||
|
let request_name = format_ident!("{}Request", self.name);
|
||||||
|
let builder_name = format_ident!("{}RequestBuilder", self.name);
|
||||||
|
let builder_mod_name = format_ident!("{}_request_builder", snake_name);
|
||||||
|
let request_mod_name = format_ident!("{snake_name}");
|
||||||
|
|
||||||
|
let request_path = quote! { crate::request::models::#request_name };
|
||||||
|
let builder_path = quote! { crate::request::models::#builder_name };
|
||||||
|
let builder_mod_path = quote! { crate::request::models::#builder_mod_name };
|
||||||
|
|
||||||
|
let tail = snake_name
|
||||||
|
.split_once('_')
|
||||||
|
.map_or_else(|| "for_selections".to_owned(), |(_, tail)| tail.to_owned());
|
||||||
|
|
||||||
|
let fn_name = format_ident!("{tail}");
|
||||||
|
|
||||||
|
for param in &self.parameters {
|
||||||
|
let (param, is_inline) = match param {
|
||||||
|
PathParameter::Inline(param) => (param, true),
|
||||||
|
PathParameter::Component(param) => (param, false),
|
||||||
|
};
|
||||||
|
|
||||||
|
if param.location == ParameterLocation::Path {
|
||||||
|
let ty = match ¶m.r#type {
|
||||||
|
ParameterType::I32 { .. } | ParameterType::Enum { .. } => {
|
||||||
|
let ty_name = format_ident!("{}", param.name);
|
||||||
|
|
||||||
|
if is_inline {
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#request_mod_name::#ty_name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
crate::parameters::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParameterType::String => quote! { String },
|
||||||
|
ParameterType::Boolean => quote! { bool },
|
||||||
|
ParameterType::Schema { type_name } => {
|
||||||
|
let ty_name = format_ident!("{}", type_name);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
crate::models::#ty_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ParameterType::Array { .. } => param.r#type.codegen_type_name(¶m.name),
|
||||||
|
};
|
||||||
|
|
||||||
|
let arg_name = format_ident!("{}", param.value.to_snake_case());
|
||||||
|
|
||||||
|
extra_args.push(quote! { #arg_name: #ty, });
|
||||||
|
disc.push(arg_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let response_ty = match &self.response {
|
||||||
|
PathResponse::Component { name } => {
|
||||||
|
let name = format_ident!("{name}");
|
||||||
|
quote! {
|
||||||
|
crate::models::#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PathResponse::ArbitraryUnion(union) => {
|
||||||
|
let name = format_ident!("{}", union.name);
|
||||||
|
quote! {
|
||||||
|
crate::request::models::#request_mod_name::#name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
pub async fn #fn_name<S>(
|
||||||
|
&self,
|
||||||
|
#(#extra_args)*
|
||||||
|
builder: impl FnOnce(
|
||||||
|
#builder_path<#builder_mod_path::Empty>
|
||||||
|
) -> #builder_path<S>,
|
||||||
|
) -> Result<#response_ty, E::Error>
|
||||||
|
where
|
||||||
|
S: #builder_mod_path::IsComplete,
|
||||||
|
{
|
||||||
|
let r = builder(#request_path::builder(#(#disc),*)).build();
|
||||||
|
|
||||||
|
self.0.fetch(r).await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PathNamespace<'r> {
|
||||||
|
path: &'r Path,
|
||||||
|
ident: Option<Ident>,
|
||||||
|
elements: Vec<TokenStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathNamespace<'_> {
|
||||||
|
pub fn get_ident(&mut self) -> Ident {
|
||||||
|
self.ident
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
let name = self.path.name.to_snake_case();
|
||||||
|
format_ident!("{name}")
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_element(&mut self, el: TokenStream) {
|
||||||
|
self.elements.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(mut self) -> Option<TokenStream> {
|
||||||
|
if self.elements.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let ident = self.get_ident();
|
||||||
|
let elements = self.elements;
|
||||||
|
Some(quote! {
|
||||||
|
pub mod #ident {
|
||||||
|
#(#elements)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::openapi::schema::OpenApiSchema;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_paths() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut paths = 0;
|
||||||
|
let mut unresolved = vec![];
|
||||||
|
|
||||||
|
for (name, desc) in &schema.paths {
|
||||||
|
paths += 1;
|
||||||
|
if Path::from_schema(name, desc, &schema.components.parameters).is_none() {
|
||||||
|
unresolved.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to resolve {}/{} paths. Could not resolve [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
paths,
|
||||||
|
unresolved
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| format!("`{u}`"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn codegen_paths() {
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut paths = 0;
|
||||||
|
let mut unresolved = vec![];
|
||||||
|
|
||||||
|
for (name, desc) in &schema.paths {
|
||||||
|
paths += 1;
|
||||||
|
let Some(path) = Path::from_schema(name, desc, &schema.components.parameters) else {
|
||||||
|
unresolved.push(name);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if path.codegen_scope_call().is_none() || path.codegen_request().is_none() {
|
||||||
|
unresolved.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !unresolved.is_empty() {
|
||||||
|
panic!(
|
||||||
|
"Failed to codegen {}/{} paths. Could not resolve [{}]",
|
||||||
|
unresolved.len(),
|
||||||
|
paths,
|
||||||
|
unresolved
|
||||||
|
.into_iter()
|
||||||
|
.map(|u| format!("`{u}`"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
torn-api-codegen/src/model/scope.rs
Normal file
64
torn-api-codegen/src/model/scope.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
use heck::ToUpperCamelCase;
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
|
||||||
|
use super::path::{Path, PathSegment};
|
||||||
|
|
||||||
|
pub struct Scope {
|
||||||
|
pub name: String,
|
||||||
|
pub mod_name: String,
|
||||||
|
pub members: Vec<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Scope {
|
||||||
|
pub fn from_paths(paths: Vec<Path>) -> Vec<Scope> {
|
||||||
|
let mut map = IndexMap::new();
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let Some(PathSegment::Constant(first_seg)) = path.segments.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
map.entry(first_seg.to_owned())
|
||||||
|
.or_insert_with(|| Scope {
|
||||||
|
name: format!("{}Scope", first_seg.to_upper_camel_case()),
|
||||||
|
mod_name: first_seg.clone(),
|
||||||
|
members: Vec::new(),
|
||||||
|
})
|
||||||
|
.members
|
||||||
|
.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.into_values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
|
||||||
|
let mut functions = Vec::with_capacity(self.members.len());
|
||||||
|
|
||||||
|
for member in &self.members {
|
||||||
|
if let Some(code) = member.codegen_scope_call() {
|
||||||
|
functions.push(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
pub struct #name<'e, E>(&'e E)
|
||||||
|
where
|
||||||
|
E: crate::executor::Executor;
|
||||||
|
|
||||||
|
impl<'e, E> #name<'e, E>
|
||||||
|
where
|
||||||
|
E: crate::executor::Executor
|
||||||
|
{
|
||||||
|
pub fn new(executor: &'e E) -> Self {
|
||||||
|
Self(executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#functions)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
50
torn-api-codegen/src/model/union.rs
Normal file
50
torn-api-codegen/src/model/union.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use heck::ToSnakeCase;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
|
||||||
|
use crate::openapi::path::OpenApiResponseBody;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Union {
|
||||||
|
pub name: String,
|
||||||
|
pub members: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Union {
|
||||||
|
pub fn from_schema(name: &str, schema: &OpenApiResponseBody) -> Option<Self> {
|
||||||
|
let members = match schema {
|
||||||
|
OpenApiResponseBody::Union { any_of } => {
|
||||||
|
any_of.iter().map(|l| l.ref_path.to_owned()).collect()
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let name = name.to_owned();
|
||||||
|
|
||||||
|
Some(Self { name, members })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn codegen(&self) -> Option<TokenStream> {
|
||||||
|
let name = format_ident!("{}", self.name);
|
||||||
|
let mut variants = Vec::new();
|
||||||
|
|
||||||
|
for member in &self.members {
|
||||||
|
let variant_name = member.strip_prefix("#/components/schemas/")?;
|
||||||
|
let accessor_name = format_ident!("{}", variant_name.to_snake_case());
|
||||||
|
let ty_name = format_ident!("{}", variant_name);
|
||||||
|
variants.push(quote! {
|
||||||
|
pub fn #accessor_name(&self) -> Result<crate::models::#ty_name, serde_json::Error> {
|
||||||
|
<crate::models::#ty_name as serde::Deserialize>::deserialize(&self.0)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
#[derive(Debug, Clone, serde::Deserialize)]
|
||||||
|
pub struct #name(serde_json::Value);
|
||||||
|
|
||||||
|
impl #name {
|
||||||
|
#(#variants)*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
4
torn-api-codegen/src/openapi/mod.rs
Normal file
4
torn-api-codegen/src/openapi/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod parameter;
|
||||||
|
pub mod path;
|
||||||
|
pub mod schema;
|
||||||
|
pub mod r#type;
|
40
torn-api-codegen/src/openapi/parameter.rs
Normal file
40
torn-api-codegen/src/openapi/parameter.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ParameterLocation {
|
||||||
|
Query,
|
||||||
|
Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum OpenApiParameterDefault<'a> {
|
||||||
|
Int(i32),
|
||||||
|
Str(&'a str),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OpenApiParameterSchema<'a> {
|
||||||
|
#[serde(rename = "$ref")]
|
||||||
|
pub ref_path: Option<&'a str>,
|
||||||
|
pub r#type: Option<&'a str>,
|
||||||
|
pub r#enum: Option<Vec<&'a str>>,
|
||||||
|
pub format: Option<&'a str>,
|
||||||
|
pub default: Option<OpenApiParameterDefault<'a>>,
|
||||||
|
pub maximum: Option<i32>,
|
||||||
|
pub minimum: Option<i32>,
|
||||||
|
pub items: Option<Box<OpenApiParameterSchema<'a>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OpenApiParameter<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub description: Option<Cow<'a, str>>,
|
||||||
|
pub r#in: ParameterLocation,
|
||||||
|
pub required: bool,
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub schema: OpenApiParameterSchema<'a>,
|
||||||
|
}
|
81
torn-api-codegen/src/openapi/path.rs
Normal file
81
torn-api-codegen/src/openapi/path.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer};
|
||||||
|
|
||||||
|
use super::parameter::OpenApiParameter;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum OpenApiPathParameter<'a> {
|
||||||
|
Link {
|
||||||
|
#[serde(rename = "$ref")]
|
||||||
|
ref_path: &'a str,
|
||||||
|
},
|
||||||
|
Inline(OpenApiParameter<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct SchemaLink<'a> {
|
||||||
|
#[serde(rename = "$ref")]
|
||||||
|
pub ref_path: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum OpenApiResponseBody<'a> {
|
||||||
|
Schema(SchemaLink<'a>),
|
||||||
|
Union {
|
||||||
|
#[serde(borrow, rename = "anyOf")]
|
||||||
|
any_of: Vec<SchemaLink<'a>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_response_body<'de, D>(deserializer: D) -> Result<OpenApiResponseBody<'de>, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Json<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
schema: OpenApiResponseBody<'a>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Content<'a> {
|
||||||
|
#[serde(borrow, rename = "application/json")]
|
||||||
|
json: Json<'a>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct StatusOk<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
content: Content<'a>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Responses<'a> {
|
||||||
|
#[serde(borrow, rename = "200")]
|
||||||
|
ok: StatusOk<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let responses = Responses::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
Ok(responses.ok.content.json.schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OpenApiPathBody<'a> {
|
||||||
|
pub summary: Option<Cow<'a, str>>,
|
||||||
|
pub description: Cow<'a, str>,
|
||||||
|
#[serde(borrow, default)]
|
||||||
|
pub parameters: Vec<OpenApiPathParameter<'a>>,
|
||||||
|
#[serde(
|
||||||
|
borrow,
|
||||||
|
rename = "responses",
|
||||||
|
deserialize_with = "deserialize_response_body"
|
||||||
|
)]
|
||||||
|
pub response_content: OpenApiResponseBody<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OpenApiPath<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub get: OpenApiPathBody<'a>,
|
||||||
|
}
|
38
torn-api-codegen/src/openapi/schema.rs
Normal file
38
torn-api-codegen/src/openapi/schema.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use super::{parameter::OpenApiParameter, path::OpenApiPath, r#type::OpenApiType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct Components<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub schemas: IndexMap<&'a str, OpenApiType<'a>>,
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub parameters: IndexMap<&'a str, OpenApiParameter<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct OpenApiSchema<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub paths: IndexMap<&'a str, OpenApiPath<'a>>,
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub components: Components<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenApiSchema<'_> {
|
||||||
|
pub fn read() -> Result<Self, serde_json::Error> {
|
||||||
|
let s = include_str!("../../openapi.json");
|
||||||
|
|
||||||
|
serde_json::from_str(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read() {
|
||||||
|
OpenApiSchema::read().unwrap();
|
||||||
|
}
|
||||||
|
}
|
98
torn-api-codegen/src/openapi/type.rs
Normal file
98
torn-api-codegen/src/openapi/type.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum OpenApiVariants<'a> {
|
||||||
|
Int(Vec<i32>),
|
||||||
|
#[serde(borrow)]
|
||||||
|
Str(Vec<&'a str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct OpenApiType<'a> {
|
||||||
|
#[serde(default)]
|
||||||
|
pub deprecated: bool,
|
||||||
|
pub description: Option<Cow<'a, str>>,
|
||||||
|
|
||||||
|
pub r#type: Option<&'a str>,
|
||||||
|
pub format: Option<&'a str>,
|
||||||
|
|
||||||
|
#[serde(rename = "$ref")]
|
||||||
|
pub ref_path: Option<&'a str>,
|
||||||
|
|
||||||
|
pub one_of: Option<Vec<OpenApiType<'a>>>,
|
||||||
|
pub all_of: Option<Vec<OpenApiType<'a>>>,
|
||||||
|
|
||||||
|
pub required: Option<Vec<&'a str>>,
|
||||||
|
#[serde(borrow)]
|
||||||
|
pub properties: Option<IndexMap<&'a str, OpenApiType<'a>>>,
|
||||||
|
|
||||||
|
pub items: Option<Box<OpenApiType<'a>>>,
|
||||||
|
pub r#enum: Option<OpenApiVariants<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn object() {
|
||||||
|
let json = r##"
|
||||||
|
{
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"branches"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/TornFactionTreeBranch"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
"##;
|
||||||
|
|
||||||
|
let obj: OpenApiType = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(obj.r#type, Some("object"));
|
||||||
|
|
||||||
|
let props = obj.properties.unwrap();
|
||||||
|
|
||||||
|
assert!(props.contains_key("name"));
|
||||||
|
|
||||||
|
let branches = props.get("branches").unwrap();
|
||||||
|
assert_eq!(branches.r#type, Some("array"));
|
||||||
|
|
||||||
|
let items = branches.items.as_ref().unwrap();
|
||||||
|
assert!(items.ref_path.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enum_variants() {
|
||||||
|
let int_json = r#"
|
||||||
|
[1, 2, 3, 4]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let de: OpenApiVariants = serde_json::from_str(int_json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(de, OpenApiVariants::Int(vec![1, 2, 3, 4]));
|
||||||
|
|
||||||
|
let str_json = r#"
|
||||||
|
["foo", "bar", "baz"]
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let de: OpenApiVariants = serde_json::from_str(str_json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(de, OpenApiVariants::Str(vec!["foo", "bar", "baz"]));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,51 +1,27 @@
|
||||||
[package]
|
[package]
|
||||||
name = "torn-api"
|
name = "torn-api"
|
||||||
version = "0.7.5"
|
version = "1.0.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
rust-version = "1.75.0"
|
|
||||||
authors = ["Pyrit [2111649]"]
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/TotallyNot/torn-api.rs.git"
|
|
||||||
homepage = "https://github.com/TotallyNot/torn-api.rs.git"
|
|
||||||
description = "Torn API bindings for rust"
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "deserialisation_benchmark"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = [ "reqwest", "user", "faction", "torn", "key", "market" ]
|
|
||||||
reqwest = [ "dep:reqwest" ]
|
|
||||||
awc = [ "dep:awc" ]
|
|
||||||
decimal = [ "dep:rust_decimal" ]
|
|
||||||
|
|
||||||
user = [ "__common" ]
|
|
||||||
faction = [ "__common" ]
|
|
||||||
torn = [ "__common" ]
|
|
||||||
market = [ "__common" ]
|
|
||||||
key = []
|
|
||||||
|
|
||||||
__common = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_repr = "0.1"
|
||||||
chrono = { version = "0.4.31", features = [ "serde" ], default-features = false }
|
serde_json = { workspace = true }
|
||||||
async-trait = "0.1"
|
bon = "3.6"
|
||||||
thiserror = "1"
|
bytes = "1"
|
||||||
futures = "0.3"
|
http = "1"
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
reqwest = { version = "0.12", default-features = false, features = [ "json" ], optional = true }
|
"rustls-tls",
|
||||||
awc = { version = "3", default-features = false, optional = true }
|
"json",
|
||||||
rust_decimal = { version = "1", default-features = false, optional = true, features = [ "serde" ] }
|
"brotli",
|
||||||
|
] }
|
||||||
torn-api-macros = { path = "../torn-api-macros", version = "0.3.1" }
|
thiserror = "2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = { version = "2.7.0" }
|
tokio = { version = "1", features = ["full"] }
|
||||||
dotenv = "0.15.0"
|
|
||||||
tokio = { version = "1.20.1", features = ["test-util", "rt", "macros"] }
|
[build-dependencies]
|
||||||
tokio-test = "0.4.2"
|
torn-api-codegen = { path = "../torn-api-codegen" }
|
||||||
reqwest = { version = "0.12", default-features = true }
|
syn = { workspace = true, features = ["parsing"] }
|
||||||
awc = { version = "3", features = [ "rustls" ] }
|
proc-macro2 = { workspace = true }
|
||||||
criterion = "0.5"
|
prettyplease = "0.2"
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
use criterion::{criterion_group, criterion_main, Criterion};
|
|
||||||
use torn_api::{faction, send::ApiClient, user};
|
|
||||||
|
|
||||||
pub fn user_benchmark(c: &mut Criterion) {
|
|
||||||
dotenv::dotenv().unwrap();
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_io()
|
|
||||||
.enable_time()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let response = rt.block_on(async {
|
|
||||||
let key = std::env::var("APIKEY").expect("api key");
|
|
||||||
let client = reqwest::Client::default();
|
|
||||||
|
|
||||||
client
|
|
||||||
.torn_api(key)
|
|
||||||
.user(|b| {
|
|
||||||
b.selections([
|
|
||||||
user::Selection::Basic,
|
|
||||||
user::Selection::Discord,
|
|
||||||
user::Selection::Profile,
|
|
||||||
user::Selection::PersonalStats,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("user deserialize", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
response.basic().unwrap();
|
|
||||||
response.discord().unwrap();
|
|
||||||
response.profile().unwrap();
|
|
||||||
response.personal_stats().unwrap();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn faction_benchmark(c: &mut Criterion) {
|
|
||||||
dotenv::dotenv().unwrap();
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_io()
|
|
||||||
.enable_time()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let response = rt.block_on(async {
|
|
||||||
let key = std::env::var("APIKEY").expect("api key");
|
|
||||||
let client = reqwest::Client::default();
|
|
||||||
|
|
||||||
client
|
|
||||||
.torn_api(key)
|
|
||||||
.faction(|b| b.selections([faction::Selection::Basic]))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("faction deserialize", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
response.basic().unwrap();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn attacks_full(c: &mut Criterion) {
|
|
||||||
dotenv::dotenv().unwrap();
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_io()
|
|
||||||
.enable_time()
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
let response = rt.block_on(async {
|
|
||||||
let key = std::env::var("APIKEY").expect("api key");
|
|
||||||
let client = reqwest::Client::default();
|
|
||||||
|
|
||||||
client
|
|
||||||
.torn_api(key)
|
|
||||||
.faction(|b| b.selections([faction::Selection::AttacksFull]))
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("attacksfull deserialize", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
response.attacks_full().unwrap();
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, user_benchmark, faction_benchmark, attacks_full);
|
|
||||||
criterion_main!(benches);
|
|
75
torn-api/build.rs
Normal file
75
torn-api/build.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use std::{env, fs, path::Path};
|
||||||
|
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use torn_api_codegen::{
|
||||||
|
model::{parameter::Parameter, path::Path as ApiPath, resolve, scope::Scope},
|
||||||
|
openapi::schema::OpenApiSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DENY_LIST: &[&str] = &[];
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let out_dir = env::var_os("OUT_DIR").unwrap();
|
||||||
|
let model_dest = Path::new(&out_dir).join("models.rs");
|
||||||
|
let params_dest = Path::new(&out_dir).join("parameters.rs");
|
||||||
|
let requests_dest = Path::new(&out_dir).join("requests.rs");
|
||||||
|
let scopes_dest = Path::new(&out_dir).join("scopes.rs");
|
||||||
|
|
||||||
|
let schema = OpenApiSchema::read().unwrap();
|
||||||
|
|
||||||
|
let mut models_code = TokenStream::new();
|
||||||
|
|
||||||
|
for (name, model) in &schema.components.schemas {
|
||||||
|
if DENY_LIST.contains(name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let model = resolve(model, name, &schema.components.schemas);
|
||||||
|
if let Some(new_code) = model.codegen() {
|
||||||
|
models_code.extend(new_code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let models_file = syn::parse2(models_code).unwrap();
|
||||||
|
let models_pretty = prettyplease::unparse(&models_file);
|
||||||
|
fs::write(&model_dest, models_pretty).unwrap();
|
||||||
|
|
||||||
|
let mut params_code = TokenStream::new();
|
||||||
|
|
||||||
|
for (name, param) in &schema.components.parameters {
|
||||||
|
if let Some(code) = Parameter::from_schema(name, param).unwrap().codegen() {
|
||||||
|
params_code.extend(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let params_file = syn::parse2(params_code).unwrap();
|
||||||
|
let params_pretty = prettyplease::unparse(¶ms_file);
|
||||||
|
fs::write(¶ms_dest, params_pretty).unwrap();
|
||||||
|
|
||||||
|
let mut requests_code = TokenStream::new();
|
||||||
|
let mut paths = Vec::new();
|
||||||
|
for (name, path) in &schema.paths {
|
||||||
|
let Some(path) = ApiPath::from_schema(name, path, &schema.components.parameters) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(code) = path.codegen_request() {
|
||||||
|
requests_code.extend(code);
|
||||||
|
}
|
||||||
|
paths.push(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let requests_file = syn::parse2(requests_code).unwrap();
|
||||||
|
let requests_pretty = prettyplease::unparse(&requests_file);
|
||||||
|
fs::write(&requests_dest, requests_pretty).unwrap();
|
||||||
|
|
||||||
|
let mut scope_code = TokenStream::new();
|
||||||
|
let scopes = Scope::from_paths(paths);
|
||||||
|
for scope in scopes {
|
||||||
|
if let Some(code) = scope.codegen() {
|
||||||
|
scope_code.extend(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let scopes_file = syn::parse2(scope_code).unwrap();
|
||||||
|
let scopes_pretty = prettyplease::unparse(&scopes_file);
|
||||||
|
fs::write(&scopes_dest, scopes_pretty).unwrap();
|
||||||
|
}
|
|
@ -1,22 +0,0 @@
|
||||||
use async_trait::async_trait;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use crate::local::ApiClient;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum AwcApiClientError {
|
|
||||||
#[error(transparent)]
|
|
||||||
Client(#[from] awc::error::SendRequestError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Payload(#[from] awc::error::JsonPayloadError),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
impl ApiClient for awc::Client {
|
|
||||||
type Error = AwcApiClientError;
|
|
||||||
|
|
||||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error> {
|
|
||||||
self.get(url).send().await?.json().await.map_err(Into::into)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,172 +0,0 @@
|
||||||
use chrono::{serde::ts_seconds, DateTime, Utc};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use torn_api_macros::IntoOwned;
|
|
||||||
|
|
||||||
use crate::de_util;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub enum OnlineStatus {
|
|
||||||
Online,
|
|
||||||
Offline,
|
|
||||||
Idle,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct LastAction {
|
|
||||||
#[serde(with = "ts_seconds")]
|
|
||||||
pub timestamp: DateTime<Utc>,
|
|
||||||
pub status: OnlineStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
|
||||||
pub enum State {
|
|
||||||
Okay,
|
|
||||||
Traveling,
|
|
||||||
Hospital,
|
|
||||||
Abroad,
|
|
||||||
Jail,
|
|
||||||
Federal,
|
|
||||||
Fallen,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum StateColour {
|
|
||||||
Green,
|
|
||||||
Red,
|
|
||||||
Blue,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned, Deserialize)]
|
|
||||||
pub struct Status<'a> {
|
|
||||||
pub description: &'a str,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_is_none")]
|
|
||||||
pub details: Option<&'a str>,
|
|
||||||
#[serde(rename = "color")]
|
|
||||||
pub colour: StateColour,
|
|
||||||
pub state: State,
|
|
||||||
#[serde(deserialize_with = "de_util::zero_date_is_none")]
|
|
||||||
pub until: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Territory {
|
|
||||||
pub sector: i16,
|
|
||||||
pub size: i16,
|
|
||||||
pub density: i16,
|
|
||||||
pub daily_respect: i16,
|
|
||||||
pub faction: i32,
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
#[serde(deserialize_with = "de_util::string_or_decimal")]
|
|
||||||
pub coordinate_x: rust_decimal::Decimal,
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
#[serde(deserialize_with = "de_util::string_or_decimal")]
|
|
||||||
pub coordinate_y: rust_decimal::Decimal,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
|
||||||
pub enum AttackResult {
|
|
||||||
Attacked,
|
|
||||||
Mugged,
|
|
||||||
Hospitalized,
|
|
||||||
Lost,
|
|
||||||
Arrested,
|
|
||||||
Escape,
|
|
||||||
Interrupted,
|
|
||||||
Assist,
|
|
||||||
Timeout,
|
|
||||||
Stalemate,
|
|
||||||
Special,
|
|
||||||
Looted,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Attack<'a> {
|
|
||||||
pub code: &'a str,
|
|
||||||
#[serde(with = "ts_seconds")]
|
|
||||||
pub timestamp_started: DateTime<Utc>,
|
|
||||||
#[serde(with = "ts_seconds")]
|
|
||||||
pub timestamp_ended: DateTime<Utc>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_int_option")]
|
|
||||||
pub attacker_id: Option<i32>,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_int_option")]
|
|
||||||
pub attacker_faction: Option<i32>,
|
|
||||||
pub defender_id: i32,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_int_option")]
|
|
||||||
pub defender_faction: Option<i32>,
|
|
||||||
pub result: AttackResult,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::int_is_bool")]
|
|
||||||
pub stealthed: bool,
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
pub respect: rust_decimal::Decimal,
|
|
||||||
|
|
||||||
#[cfg(not(feature = "decimal"))]
|
|
||||||
pub respect: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct RespectModifiers {
|
|
||||||
pub fair_fight: f32,
|
|
||||||
pub war: f32,
|
|
||||||
pub retaliation: f32,
|
|
||||||
pub group_attack: f32,
|
|
||||||
pub overseas: f32,
|
|
||||||
pub chain_bonus: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct AttackFull<'a> {
|
|
||||||
pub code: &'a str,
|
|
||||||
#[serde(with = "ts_seconds")]
|
|
||||||
pub timestamp_started: DateTime<Utc>,
|
|
||||||
#[serde(with = "ts_seconds")]
|
|
||||||
pub timestamp_ended: DateTime<Utc>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_int_option")]
|
|
||||||
pub attacker_id: Option<i32>,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_is_none")]
|
|
||||||
pub attacker_name: Option<&'a str>,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_int_option")]
|
|
||||||
pub attacker_faction: Option<i32>,
|
|
||||||
#[serde(
|
|
||||||
deserialize_with = "de_util::empty_string_is_none",
|
|
||||||
rename = "attacker_factionname"
|
|
||||||
)]
|
|
||||||
pub attacker_faction_name: Option<&'a str>,
|
|
||||||
|
|
||||||
pub defender_id: i32,
|
|
||||||
pub defender_name: &'a str,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_int_option")]
|
|
||||||
pub defender_faction: Option<i32>,
|
|
||||||
#[serde(
|
|
||||||
deserialize_with = "de_util::empty_string_is_none",
|
|
||||||
rename = "defender_factionname"
|
|
||||||
)]
|
|
||||||
pub defender_faction_name: Option<&'a str>,
|
|
||||||
|
|
||||||
pub result: AttackResult,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::int_is_bool")]
|
|
||||||
pub stealthed: bool,
|
|
||||||
#[serde(deserialize_with = "de_util::int_is_bool")]
|
|
||||||
pub raid: bool,
|
|
||||||
#[serde(deserialize_with = "de_util::int_is_bool")]
|
|
||||||
pub ranked_war: bool,
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
pub respect: rust_decimal::Decimal,
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
pub respect_loss: rust_decimal::Decimal,
|
|
||||||
|
|
||||||
#[cfg(not(feature = "decimal"))]
|
|
||||||
pub respect: f32,
|
|
||||||
#[cfg(not(feature = "decimal"))]
|
|
||||||
pub respect_loss: f32,
|
|
||||||
|
|
||||||
pub modifiers: RespectModifiers,
|
|
||||||
}
|
|
|
@ -1,245 +0,0 @@
|
||||||
#![allow(unused)]
|
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
|
|
||||||
use chrono::{serde::ts_nanoseconds::deserialize, DateTime, NaiveDateTime, Utc};
|
|
||||||
use serde::de::{Deserialize, Deserializer, Error, Unexpected, Visitor};
|
|
||||||
|
|
||||||
pub(crate) fn empty_string_is_none<'de, D>(deserializer: D) -> Result<Option<&'de str>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s: &str = Deserialize::deserialize(deserializer)?;
|
|
||||||
if s.is_empty() {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Ok(Some(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn string_is_long<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let s = String::deserialize(deserializer)?;
|
|
||||||
if s.is_empty() {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
s.parse()
|
|
||||||
.map(Some)
|
|
||||||
.map_err(|_e| Error::invalid_type(Unexpected::Str(&s), &"i64"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn zero_date_is_none<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let i = i64::deserialize(deserializer)?;
|
|
||||||
if i == 0 {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Ok(DateTime::from_timestamp(i, 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn int_is_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let i = i64::deserialize(deserializer)?;
|
|
||||||
|
|
||||||
match i {
|
|
||||||
0 => Ok(false),
|
|
||||||
1 => Ok(true),
|
|
||||||
x => Err(Error::invalid_value(Unexpected::Signed(x), &"0 or 1")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn empty_string_int_option<'de, D>(deserializer: D) -> Result<Option<i32>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct DumbVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for DumbVisitor {
|
|
||||||
type Value = Option<i32>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "Empty string or integer")
|
|
||||||
}
|
|
||||||
|
|
||||||
// serde_json will treat all unsigned integers as u64
|
|
||||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Ok(Some(v as i32))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
if v.is_empty() {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Err(E::invalid_value(Unexpected::Str(v), &self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(DumbVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn datetime_map<'de, D>(
|
|
||||||
deserializer: D,
|
|
||||||
) -> Result<BTreeMap<i32, chrono::DateTime<chrono::Utc>>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(serde::Deserialize)]
|
|
||||||
struct UnixTimestamp(
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")] chrono::DateTime<chrono::Utc>,
|
|
||||||
);
|
|
||||||
|
|
||||||
struct MapVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for MapVisitor {
|
|
||||||
type Value = BTreeMap<i32, chrono::DateTime<chrono::Utc>>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "map of unix timestamps")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: serde::de::MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut result = BTreeMap::new();
|
|
||||||
while let Some(key) = map.next_key::<&'de str>()? {
|
|
||||||
let id = key
|
|
||||||
.parse()
|
|
||||||
.map_err(|_e| A::Error::invalid_value(Unexpected::Str(key), &"integer"))?;
|
|
||||||
|
|
||||||
let ts: UnixTimestamp = map.next_value()?;
|
|
||||||
result.insert(id, ts.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_map(MapVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn empty_dict_is_empty_array<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
T: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
struct ArrayVisitor<T>(std::marker::PhantomData<T>);
|
|
||||||
|
|
||||||
impl<'de, T> Visitor<'de> for ArrayVisitor<T>
|
|
||||||
where
|
|
||||||
T: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
type Value = Vec<T>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "vec or empty object")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: serde::de::MapAccess<'de>,
|
|
||||||
{
|
|
||||||
match map.size_hint() {
|
|
||||||
Some(0) | None => Ok(Vec::default()),
|
|
||||||
Some(len) => Err(A::Error::invalid_length(len, &"empty dict")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: serde::de::SeqAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut result = match seq.size_hint() {
|
|
||||||
Some(len) => Vec::with_capacity(len),
|
|
||||||
None => Vec::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
while let Some(element) = seq.next_element()? {
|
|
||||||
result.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(ArrayVisitor(std::marker::PhantomData))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn zero_is_none<'de, D, I>(deserializer: D) -> Result<Option<I>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
I: TryFrom<i64>,
|
|
||||||
{
|
|
||||||
let num = i64::deserialize(deserializer)?;
|
|
||||||
|
|
||||||
if num == 0 {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Ok(Some(num.try_into().map_err(|_| {
|
|
||||||
D::Error::invalid_value(Unexpected::Signed(num), &std::any::type_name::<I>())
|
|
||||||
})?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn null_is_empty_dict<'de, D, K, V>(deserializer: D) -> Result<HashMap<K, V>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
K: std::hash::Hash + std::cmp::Eq + Deserialize<'de>,
|
|
||||||
V: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
pub(crate) fn string_or_decimal<'de, D>(deserializer: D) -> Result<rust_decimal::Decimal, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct DumbVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for DumbVisitor {
|
|
||||||
type Value = rust_decimal::Decimal;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "integer or float as string")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Ok(v.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
Ok(v.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: Error,
|
|
||||||
{
|
|
||||||
rust_decimal::Decimal::from_str_exact(v).map_err(E::custom)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_any(DumbVisitor)
|
|
||||||
}
|
|
156
torn-api/src/executor.rs
Normal file
156
torn-api/src/executor.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
use http::{HeaderMap, HeaderValue, header::AUTHORIZATION};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
request::{ApiResponse, IntoRequest},
|
||||||
|
scopes::{FactionScope, ForumScope, MarketScope, RacingScope, TornScope, UserScope},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait Executor {
|
||||||
|
type Error: From<serde_json::Error> + From<crate::ApiError> + Send;
|
||||||
|
|
||||||
|
fn execute<R>(
|
||||||
|
&self,
|
||||||
|
request: R,
|
||||||
|
) -> impl Future<Output = Result<ApiResponse<R::Discriminant>, Self::Error>> + Send
|
||||||
|
where
|
||||||
|
R: IntoRequest;
|
||||||
|
|
||||||
|
fn fetch<R>(&self, request: R) -> impl Future<Output = Result<R::Response, Self::Error>> + Send
|
||||||
|
where
|
||||||
|
R: IntoRequest,
|
||||||
|
{
|
||||||
|
// HACK: workaround for not using `async` in trait declaration.
|
||||||
|
// The future is `Send` but `&self` might not be.
|
||||||
|
let fut = self.execute(request);
|
||||||
|
async {
|
||||||
|
let resp = fut.await?;
|
||||||
|
|
||||||
|
let bytes = resp.body.unwrap();
|
||||||
|
|
||||||
|
if bytes.starts_with(br#"{"error":{"#) {
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorBody<'a> {
|
||||||
|
code: u16,
|
||||||
|
error: &'a str,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorContainer<'a> {
|
||||||
|
#[serde(borrow)]
|
||||||
|
error: ErrorBody<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let error: ErrorContainer = serde_json::from_slice(&bytes)?;
|
||||||
|
return Err(crate::ApiError::new(error.error.code, error.error.error).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = serde_json::from_slice(&bytes)?;
|
||||||
|
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReqwestClient(reqwest::Client);
|
||||||
|
|
||||||
|
impl ReqwestClient {
|
||||||
|
pub fn new(api_key: &str) -> Self {
|
||||||
|
let mut headers = HeaderMap::with_capacity(1);
|
||||||
|
headers.insert(
|
||||||
|
AUTHORIZATION,
|
||||||
|
HeaderValue::from_str(&format!("ApiKey {api_key}")).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.default_headers(headers)
|
||||||
|
.brotli(true)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Self(client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ExecutorExt: Executor + Sized {
|
||||||
|
fn user(&self) -> UserScope<'_, Self>;
|
||||||
|
|
||||||
|
fn faction(&self) -> FactionScope<'_, Self>;
|
||||||
|
|
||||||
|
fn torn(&self) -> TornScope<'_, Self>;
|
||||||
|
|
||||||
|
fn market(&self) -> MarketScope<'_, Self>;
|
||||||
|
|
||||||
|
fn racing(&self) -> RacingScope<'_, Self>;
|
||||||
|
|
||||||
|
fn forum(&self) -> ForumScope<'_, Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ExecutorExt for T
|
||||||
|
where
|
||||||
|
T: Executor + Sized,
|
||||||
|
{
|
||||||
|
fn user(&self) -> UserScope<'_, Self> {
|
||||||
|
UserScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn faction(&self) -> FactionScope<'_, Self> {
|
||||||
|
FactionScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn torn(&self) -> TornScope<'_, Self> {
|
||||||
|
TornScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn market(&self) -> MarketScope<'_, Self> {
|
||||||
|
MarketScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn racing(&self) -> RacingScope<'_, Self> {
|
||||||
|
RacingScope::new(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn forum(&self) -> ForumScope<'_, Self> {
|
||||||
|
ForumScope::new(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Executor for ReqwestClient {
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
async fn execute<R>(&self, request: R) -> Result<ApiResponse<R::Discriminant>, Self::Error>
|
||||||
|
where
|
||||||
|
R: IntoRequest,
|
||||||
|
{
|
||||||
|
let request = request.into_request();
|
||||||
|
let url = request.url();
|
||||||
|
|
||||||
|
let response = self.0.get(url).send().await?;
|
||||||
|
let status = response.status();
|
||||||
|
let body = response.bytes().await.ok();
|
||||||
|
|
||||||
|
Ok(ApiResponse {
|
||||||
|
discriminant: request.disriminant,
|
||||||
|
status,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::{ApiError, Error, scopes::test::test_client};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn api_error() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let resp = client.faction().basic_for_id((-1).into(), |b| b).await;
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
Err(Error::Api(ApiError::IncorrectIdEntityRelation)) => (),
|
||||||
|
other => panic!("Expected incorrect id entity relation error, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,278 +0,0 @@
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
|
|
||||||
use chrono::{DateTime, TimeZone, Utc};
|
|
||||||
use serde::{
|
|
||||||
de::{Error, Unexpected, Visitor},
|
|
||||||
Deserialize, Deserializer,
|
|
||||||
};
|
|
||||||
|
|
||||||
use torn_api_macros::{ApiCategory, IntoOwned};
|
|
||||||
|
|
||||||
use crate::de_util::{self, null_is_empty_dict};
|
|
||||||
|
|
||||||
pub use crate::common::{Attack, AttackFull, LastAction, Status, Territory};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, ApiCategory)]
|
|
||||||
#[api(category = "faction")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum FactionSelection {
|
|
||||||
#[api(type = "Basic", flatten)]
|
|
||||||
Basic,
|
|
||||||
|
|
||||||
#[api(type = "BTreeMap<i32, Attack>", field = "attacks")]
|
|
||||||
AttacksFull,
|
|
||||||
|
|
||||||
#[api(type = "BTreeMap<i32, AttackFull>", field = "attacks")]
|
|
||||||
Attacks,
|
|
||||||
|
|
||||||
#[api(
|
|
||||||
type = "HashMap<String, Territory>",
|
|
||||||
field = "territory",
|
|
||||||
with = "null_is_empty_dict"
|
|
||||||
)]
|
|
||||||
Territory,
|
|
||||||
|
|
||||||
#[api(type = "Option<Chain>", field = "chain", with = "deserialize_chain")]
|
|
||||||
Chain,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Selection = FactionSelection;
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned, Deserialize)]
|
|
||||||
pub struct Member<'a> {
|
|
||||||
pub name: &'a str,
|
|
||||||
pub level: i16,
|
|
||||||
pub days_in_faction: i16,
|
|
||||||
pub position: &'a str,
|
|
||||||
pub status: Status<'a>,
|
|
||||||
pub last_action: LastAction,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned, Deserialize)]
|
|
||||||
pub struct FactionTerritoryWar<'a> {
|
|
||||||
pub territory_war_id: i32,
|
|
||||||
pub territory: &'a str,
|
|
||||||
pub assaulting_faction: i32,
|
|
||||||
pub defending_faction: i32,
|
|
||||||
pub score: i32,
|
|
||||||
pub required_score: i32,
|
|
||||||
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub start_time: DateTime<Utc>,
|
|
||||||
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub end_time: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned, Deserialize)]
|
|
||||||
pub struct Basic<'a> {
|
|
||||||
#[serde(rename = "ID")]
|
|
||||||
pub id: i32,
|
|
||||||
pub name: &'a str,
|
|
||||||
pub leader: i32,
|
|
||||||
|
|
||||||
pub respect: i32,
|
|
||||||
pub age: i16,
|
|
||||||
pub capacity: i16,
|
|
||||||
pub best_chain: i32,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_is_none")]
|
|
||||||
pub tag_image: Option<&'a str>,
|
|
||||||
|
|
||||||
#[serde(borrow)]
|
|
||||||
pub members: BTreeMap<i32, Member<'a>>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::datetime_map")]
|
|
||||||
pub peace: BTreeMap<i32, DateTime<Utc>>,
|
|
||||||
|
|
||||||
#[serde(borrow, deserialize_with = "de_util::empty_dict_is_empty_array")]
|
|
||||||
pub territory_wars: Vec<FactionTerritoryWar<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Chain {
|
|
||||||
pub current: i32,
|
|
||||||
pub max: i32,
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
pub modifier: rust_decimal::Decimal,
|
|
||||||
pub timeout: Option<i32>,
|
|
||||||
pub cooldown: Option<i32>,
|
|
||||||
pub start: DateTime<Utc>,
|
|
||||||
pub end: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_chain<'de, D>(deserializer: D) -> Result<Option<Chain>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct ChainVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for ChainVisitor {
|
|
||||||
type Value = Option<Chain>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("struct Chain")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: serde::de::MapAccess<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
enum Fields {
|
|
||||||
Current,
|
|
||||||
Max,
|
|
||||||
Modifier,
|
|
||||||
Timeout,
|
|
||||||
Cooldown,
|
|
||||||
Start,
|
|
||||||
End,
|
|
||||||
#[serde(other)]
|
|
||||||
Ignore,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut current = None;
|
|
||||||
let mut max = None;
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
let mut modifier = None;
|
|
||||||
let mut timeout = None;
|
|
||||||
let mut cooldown = None;
|
|
||||||
let mut start = None;
|
|
||||||
let mut end = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
|
||||||
match key {
|
|
||||||
Fields::Current => {
|
|
||||||
let value = map.next_value()?;
|
|
||||||
if value != 0 {
|
|
||||||
current = Some(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Fields::Max => {
|
|
||||||
max = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Fields::Modifier => {
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
{
|
|
||||||
modifier = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Fields::Timeout => {
|
|
||||||
match map.next_value()? {
|
|
||||||
0 => timeout = Some(None),
|
|
||||||
val => timeout = Some(Some(val)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Fields::Cooldown => {
|
|
||||||
match map.next_value()? {
|
|
||||||
0 => cooldown = Some(None),
|
|
||||||
val => cooldown = Some(Some(val)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Fields::Start => {
|
|
||||||
let ts: i64 = map.next_value()?;
|
|
||||||
start = Some(Utc.timestamp_opt(ts, 0).single().ok_or_else(|| {
|
|
||||||
A::Error::invalid_value(Unexpected::Signed(ts), &"Epoch timestamp")
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
Fields::End => {
|
|
||||||
let ts: i64 = map.next_value()?;
|
|
||||||
end = Some(Utc.timestamp_opt(ts, 0).single().ok_or_else(|| {
|
|
||||||
A::Error::invalid_value(Unexpected::Signed(ts), &"Epoch timestamp")
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
Fields::Ignore => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(current) = current else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let max = max.ok_or_else(|| A::Error::missing_field("max"))?;
|
|
||||||
let timeout = timeout.ok_or_else(|| A::Error::missing_field("timeout"))?;
|
|
||||||
let cooldown = cooldown.ok_or_else(|| A::Error::missing_field("cooldown"))?;
|
|
||||||
let start = start.ok_or_else(|| A::Error::missing_field("start"))?;
|
|
||||||
let end = end.ok_or_else(|| A::Error::missing_field("end"))?;
|
|
||||||
|
|
||||||
Ok(Some(Chain {
|
|
||||||
current,
|
|
||||||
max,
|
|
||||||
#[cfg(feature = "decimal")]
|
|
||||||
modifier: modifier.ok_or_else(|| A::Error::missing_field("modifier"))?,
|
|
||||||
timeout,
|
|
||||||
cooldown,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_map(ChainVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::tests::{async_test, setup, Client, ClientTrait};
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn faction() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.faction(|b| {
|
|
||||||
b.selections([
|
|
||||||
Selection::Basic,
|
|
||||||
Selection::Attacks,
|
|
||||||
Selection::Territory,
|
|
||||||
Selection::Chain,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
response.basic().unwrap();
|
|
||||||
response.attacks().unwrap();
|
|
||||||
response.attacks_full().unwrap();
|
|
||||||
response.territory().unwrap();
|
|
||||||
response.chain().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn faction_public() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.faction(|b| {
|
|
||||||
b.id(7049)
|
|
||||||
.selections([Selection::Basic, Selection::Territory, Selection::Chain])
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
response.basic().unwrap();
|
|
||||||
response.territory().unwrap();
|
|
||||||
response.chain().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn destroyed_faction() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.faction(|b| {
|
|
||||||
b.id(8981)
|
|
||||||
.selections([Selection::Basic, Selection::Territory, Selection::Chain])
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
response.basic().unwrap();
|
|
||||||
response.territory().unwrap();
|
|
||||||
assert!(response.chain().unwrap().is_none());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
pub use torn_api_macros::IntoOwned;
|
|
||||||
|
|
||||||
pub trait IntoOwned {
|
|
||||||
type Owned;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IntoOwned for Option<T>
|
|
||||||
where
|
|
||||||
T: IntoOwned,
|
|
||||||
{
|
|
||||||
type Owned = Option<T::Owned>;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
self.map(IntoOwned::into_owned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IntoOwned for Vec<T> where T: IntoOwned {
|
|
||||||
type Owned = Vec<<T as IntoOwned>::Owned>;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
let mut owned = Vec::with_capacity(self.len());
|
|
||||||
for elem in self {
|
|
||||||
owned.push(elem.into_owned());
|
|
||||||
}
|
|
||||||
owned
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> IntoOwned for std::collections::HashMap<K, V> where V: IntoOwned, K: Eq + std::hash::Hash {
|
|
||||||
type Owned = std::collections::HashMap<K, <V as IntoOwned>::Owned>;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
self.into_iter().map(|(k, v)| (k, v.into_owned())).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<K, V> IntoOwned for std::collections::BTreeMap<K, V> where V: IntoOwned, K: Eq + Ord + std::hash::Hash {
|
|
||||||
type Owned = std::collections::BTreeMap<K, <V as IntoOwned>::Owned>;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
self.into_iter().map(|(k, v)| (k, v.into_owned())).collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<Z> IntoOwned for chrono::DateTime<Z> where Z: chrono::TimeZone {
|
|
||||||
type Owned = Self;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoOwned for &'a str {
|
|
||||||
type Owned = String;
|
|
||||||
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
self.to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_ident {
|
|
||||||
($name:path) => {
|
|
||||||
impl IntoOwned for $name {
|
|
||||||
type Owned = $name;
|
|
||||||
fn into_owned(self) -> Self::Owned {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_ident!(i64);
|
|
||||||
impl_ident!(i32);
|
|
||||||
impl_ident!(i16);
|
|
||||||
impl_ident!(i8);
|
|
||||||
impl_ident!(String);
|
|
|
@ -1,256 +0,0 @@
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use torn_api_macros::ApiCategory;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, ApiCategory)]
|
|
||||||
#[api(category = "key")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Selection {
|
|
||||||
#[api(type = "Info", flatten)]
|
|
||||||
Info,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum AccessType {
|
|
||||||
#[serde(rename = "Custom")]
|
|
||||||
Custom,
|
|
||||||
|
|
||||||
#[serde(rename = "Public Only")]
|
|
||||||
Public,
|
|
||||||
|
|
||||||
#[serde(rename = "Minimal Access")]
|
|
||||||
Minimal,
|
|
||||||
|
|
||||||
#[serde(rename = "Limited Access")]
|
|
||||||
Limited,
|
|
||||||
|
|
||||||
#[serde(rename = "Full Access")]
|
|
||||||
Full,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum KeySelection {
|
|
||||||
Info,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum UserSelection {
|
|
||||||
Ammo,
|
|
||||||
Attacks,
|
|
||||||
AttacksFull,
|
|
||||||
Bars,
|
|
||||||
Basic,
|
|
||||||
BattleStats,
|
|
||||||
Bazaar,
|
|
||||||
Cooldowns,
|
|
||||||
Crimes,
|
|
||||||
Discord,
|
|
||||||
Display,
|
|
||||||
Education,
|
|
||||||
Events,
|
|
||||||
Gym,
|
|
||||||
Hof,
|
|
||||||
Honors,
|
|
||||||
Icons,
|
|
||||||
Inventory,
|
|
||||||
JobPoints,
|
|
||||||
Log,
|
|
||||||
Medals,
|
|
||||||
Merits,
|
|
||||||
Messages,
|
|
||||||
Missions,
|
|
||||||
Money,
|
|
||||||
Networth,
|
|
||||||
NewEvents,
|
|
||||||
NewMessages,
|
|
||||||
Notifications,
|
|
||||||
Perks,
|
|
||||||
PersonalStats,
|
|
||||||
Profile,
|
|
||||||
Properties,
|
|
||||||
ReceivedEvents,
|
|
||||||
Refills,
|
|
||||||
Reports,
|
|
||||||
Revives,
|
|
||||||
RevivesFull,
|
|
||||||
Skills,
|
|
||||||
Stocks,
|
|
||||||
Timestamp,
|
|
||||||
Travel,
|
|
||||||
WeaponExp,
|
|
||||||
WorkStats,
|
|
||||||
Lookup,
|
|
||||||
PublicStatus,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum FactionSelection {
|
|
||||||
Applications,
|
|
||||||
Armor,
|
|
||||||
ArmoryNews,
|
|
||||||
AttackNews,
|
|
||||||
Attacks,
|
|
||||||
AttacksFull,
|
|
||||||
Basic,
|
|
||||||
Boosters,
|
|
||||||
Cesium,
|
|
||||||
Chain,
|
|
||||||
ChainReport,
|
|
||||||
Chains,
|
|
||||||
Contributors,
|
|
||||||
Crimenews,
|
|
||||||
Crimes,
|
|
||||||
Currency,
|
|
||||||
Donations,
|
|
||||||
Drugs,
|
|
||||||
FundsNews,
|
|
||||||
MainNews,
|
|
||||||
Medical,
|
|
||||||
MembershipNews,
|
|
||||||
Positions,
|
|
||||||
Reports,
|
|
||||||
Revives,
|
|
||||||
RevivesFull,
|
|
||||||
Stats,
|
|
||||||
Temporary,
|
|
||||||
Territory,
|
|
||||||
TerritoryNews,
|
|
||||||
Timestamp,
|
|
||||||
Upgrades,
|
|
||||||
Weapons,
|
|
||||||
Lookup,
|
|
||||||
Caches,
|
|
||||||
CrimeExp,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum CompanySelection {
|
|
||||||
Applications,
|
|
||||||
Companies,
|
|
||||||
Detailed,
|
|
||||||
Employees,
|
|
||||||
News,
|
|
||||||
NewsFull,
|
|
||||||
Profile,
|
|
||||||
Stock,
|
|
||||||
Timestamp,
|
|
||||||
Lookup,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum TornSelection {
|
|
||||||
Bank,
|
|
||||||
Cards,
|
|
||||||
ChainReport,
|
|
||||||
Companies,
|
|
||||||
Competition,
|
|
||||||
Education,
|
|
||||||
FactionTree,
|
|
||||||
Gyms,
|
|
||||||
Honors,
|
|
||||||
Items,
|
|
||||||
ItemStats,
|
|
||||||
LogCategories,
|
|
||||||
LogTypes,
|
|
||||||
Medals,
|
|
||||||
OrganisedCrimes,
|
|
||||||
PawnShop,
|
|
||||||
PokerTables,
|
|
||||||
Properties,
|
|
||||||
Rackets,
|
|
||||||
Raids,
|
|
||||||
RankedWars,
|
|
||||||
RankedWarReport,
|
|
||||||
Stats,
|
|
||||||
Stocks,
|
|
||||||
Territory,
|
|
||||||
TerritoryWars,
|
|
||||||
Timestamp,
|
|
||||||
Lookup,
|
|
||||||
CityShops,
|
|
||||||
ItemDetails,
|
|
||||||
TerritoryNames,
|
|
||||||
TerritoryWarReport,
|
|
||||||
RaidReport,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum MarketSelection {
|
|
||||||
Bazaar,
|
|
||||||
ItemMarket,
|
|
||||||
PointsMarket,
|
|
||||||
Timestamp,
|
|
||||||
Lookup,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum PropertySelection {
|
|
||||||
Property,
|
|
||||||
Timestamp,
|
|
||||||
Lookup,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Selections {
|
|
||||||
pub user: HashSet<UserSelection>,
|
|
||||||
pub faction: HashSet<FactionSelection>,
|
|
||||||
pub company: HashSet<CompanySelection>,
|
|
||||||
pub torn: HashSet<TornSelection>,
|
|
||||||
pub market: HashSet<MarketSelection>,
|
|
||||||
pub property: HashSet<PropertySelection>,
|
|
||||||
pub key: HashSet<KeySelection>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Info {
|
|
||||||
pub access_level: i16,
|
|
||||||
pub access_type: AccessType,
|
|
||||||
pub selections: Selections,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::tests::{async_test, setup, Client, ClientTrait};
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn key() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.key(|b| b.selections([Selection::Info]))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
response.info().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,460 +1,150 @@
|
||||||
#![warn(clippy::all, clippy::perf, clippy::style, clippy::suspicious)]
|
|
||||||
|
|
||||||
pub mod into_owned;
|
|
||||||
pub mod local;
|
|
||||||
pub mod send;
|
|
||||||
|
|
||||||
#[cfg(feature = "user")]
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
#[cfg(feature = "faction")]
|
|
||||||
pub mod faction;
|
|
||||||
|
|
||||||
#[cfg(feature = "market")]
|
|
||||||
pub mod market;
|
|
||||||
|
|
||||||
#[cfg(feature = "torn")]
|
|
||||||
pub mod torn;
|
|
||||||
|
|
||||||
#[cfg(feature = "key")]
|
|
||||||
pub mod key;
|
|
||||||
|
|
||||||
#[cfg(feature = "awc")]
|
|
||||||
pub mod awc;
|
|
||||||
|
|
||||||
#[cfg(feature = "reqwest")]
|
|
||||||
pub mod reqwest;
|
|
||||||
|
|
||||||
#[cfg(feature = "__common")]
|
|
||||||
pub mod common;
|
|
||||||
|
|
||||||
mod de_util;
|
|
||||||
|
|
||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{de::Error as DeError, Deserialize};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
pub use into_owned::IntoOwned;
|
pub mod executor;
|
||||||
|
pub mod models;
|
||||||
|
pub mod parameters;
|
||||||
|
pub mod request;
|
||||||
|
pub mod scopes;
|
||||||
|
|
||||||
pub struct ApiResponse {
|
#[derive(Debug, Error, Clone, PartialEq, Eq)]
|
||||||
pub value: serde_json::Value,
|
pub enum ApiError {
|
||||||
|
#[error("Unhandled error, should not occur")]
|
||||||
|
Unknown,
|
||||||
|
#[error("Private key is empty in current request")]
|
||||||
|
KeyIsEmpty,
|
||||||
|
#[error("Private key is wrong/incorrect format")]
|
||||||
|
IncorrectKey,
|
||||||
|
#[error("Requesting an incorrect basic type")]
|
||||||
|
WrongType,
|
||||||
|
#[error("Requesting incorect selection fields")]
|
||||||
|
WrongFields,
|
||||||
|
#[error(
|
||||||
|
"Requests are blocked for a small period of time because of too many requests per user"
|
||||||
|
)]
|
||||||
|
TooManyRequest,
|
||||||
|
#[error("Wrong ID value")]
|
||||||
|
IncorrectId,
|
||||||
|
#[error("A requested selection is private")]
|
||||||
|
IncorrectIdEntityRelation,
|
||||||
|
#[error("Current IP is banned for a small period of time because of abuse")]
|
||||||
|
IpBlock,
|
||||||
|
#[error("Api system is currently disabled")]
|
||||||
|
ApiDisabled,
|
||||||
|
#[error("Current key can't be used because owner is in federal jail")]
|
||||||
|
KeyOwnerInFederalJail,
|
||||||
|
#[error("You can only change your API key once every 60 seconds")]
|
||||||
|
KeyChange,
|
||||||
|
#[error("Error reading key from Database")]
|
||||||
|
KeyRead,
|
||||||
|
#[error("The key owner hasn't been online for more than 7 days")]
|
||||||
|
TemporaryInactivity,
|
||||||
|
#[error("Too many records have been pulled today by this user from our cloud services")]
|
||||||
|
DailyReadLimit,
|
||||||
|
#[error("An error code specifically for testing purposes that has no dedicated meaning")]
|
||||||
|
TemporaryError,
|
||||||
|
#[error("A selection is being called of which this key does not have permission to access")]
|
||||||
|
InsufficientAccessLevel,
|
||||||
|
#[error("Backend error occurred, please try again")]
|
||||||
|
Backend,
|
||||||
|
#[error("API key has been paused by the owner")]
|
||||||
|
Paused,
|
||||||
|
#[error("Must be migrated to crimes 2.0")]
|
||||||
|
NotMigratedCrimes,
|
||||||
|
#[error("Race not yet finished")]
|
||||||
|
RaceNotFinished,
|
||||||
|
#[error("Wrong cat value")]
|
||||||
|
IncorrectCategory,
|
||||||
|
#[error("This selection is only available in API v1")]
|
||||||
|
OnlyInV1,
|
||||||
|
#[error("This selection is only available in API v2")]
|
||||||
|
OnlyInV2,
|
||||||
|
#[error("Closed temporarily")]
|
||||||
|
ClosedTemporarily,
|
||||||
|
#[error("Other: {message}")]
|
||||||
|
Other { code: u16, message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
impl ApiError {
|
||||||
pub enum ResponseError {
|
pub fn new(code: u16, message: &str) -> Self {
|
||||||
#[error("API: {reason}")]
|
match code {
|
||||||
Api { code: u8, reason: String },
|
0 => Self::Unknown,
|
||||||
|
1 => Self::KeyIsEmpty,
|
||||||
|
2 => Self::IncorrectKey,
|
||||||
|
3 => Self::WrongType,
|
||||||
|
4 => Self::WrongFields,
|
||||||
|
5 => Self::TooManyRequest,
|
||||||
|
6 => Self::IncorrectId,
|
||||||
|
7 => Self::IncorrectIdEntityRelation,
|
||||||
|
8 => Self::IpBlock,
|
||||||
|
9 => Self::ApiDisabled,
|
||||||
|
10 => Self::KeyOwnerInFederalJail,
|
||||||
|
11 => Self::KeyChange,
|
||||||
|
12 => Self::KeyRead,
|
||||||
|
13 => Self::TemporaryInactivity,
|
||||||
|
14 => Self::DailyReadLimit,
|
||||||
|
15 => Self::TemporaryError,
|
||||||
|
16 => Self::InsufficientAccessLevel,
|
||||||
|
17 => Self::Backend,
|
||||||
|
18 => Self::Paused,
|
||||||
|
19 => Self::NotMigratedCrimes,
|
||||||
|
20 => Self::RaceNotFinished,
|
||||||
|
21 => Self::IncorrectCategory,
|
||||||
|
22 => Self::OnlyInV1,
|
||||||
|
23 => Self::OnlyInV2,
|
||||||
|
24 => Self::ClosedTemporarily,
|
||||||
|
other => Self::Other {
|
||||||
|
code: other,
|
||||||
|
message: message.to_owned(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[error(transparent)]
|
pub fn code(&self) -> u16 {
|
||||||
MalformedResponse(#[from] serde_json::Error),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ResponseError {
|
|
||||||
pub fn api_code(&self) -> Option<u8> {
|
|
||||||
match self {
|
match self {
|
||||||
Self::Api { code, .. } => Some(*code),
|
Self::Unknown => 0,
|
||||||
_ => None,
|
Self::KeyIsEmpty => 1,
|
||||||
|
Self::IncorrectKey => 2,
|
||||||
|
Self::WrongType => 3,
|
||||||
|
Self::WrongFields => 4,
|
||||||
|
Self::TooManyRequest => 5,
|
||||||
|
Self::IncorrectId => 6,
|
||||||
|
Self::IncorrectIdEntityRelation => 7,
|
||||||
|
Self::IpBlock => 8,
|
||||||
|
Self::ApiDisabled => 9,
|
||||||
|
Self::KeyOwnerInFederalJail => 10,
|
||||||
|
Self::KeyChange => 11,
|
||||||
|
Self::KeyRead => 12,
|
||||||
|
Self::TemporaryInactivity => 13,
|
||||||
|
Self::DailyReadLimit => 14,
|
||||||
|
Self::TemporaryError => 15,
|
||||||
|
Self::InsufficientAccessLevel => 16,
|
||||||
|
Self::Backend => 17,
|
||||||
|
Self::Paused => 18,
|
||||||
|
Self::NotMigratedCrimes => 19,
|
||||||
|
Self::RaceNotFinished => 20,
|
||||||
|
Self::IncorrectCategory => 21,
|
||||||
|
Self::OnlyInV1 => 22,
|
||||||
|
Self::OnlyInV2 => 23,
|
||||||
|
Self::ClosedTemporarily => 24,
|
||||||
|
Self::Other { code, .. } => *code,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ApiResponse {
|
#[derive(Debug, Error, PartialEq, Eq)]
|
||||||
pub fn from_value(mut value: serde_json::Value) -> Result<Self, ResponseError> {
|
pub enum ParameterError {
|
||||||
#[derive(serde::Deserialize)]
|
#[error("value `{value}` is out of range for parameter {name}")]
|
||||||
struct ApiErrorDto {
|
OutOfRange { name: &'static str, value: i32 },
|
||||||
code: u8,
|
|
||||||
#[serde(rename = "error")]
|
|
||||||
reason: String,
|
|
||||||
}
|
|
||||||
match value.get_mut("error") {
|
|
||||||
Some(error) => {
|
|
||||||
let dto: ApiErrorDto = serde_json::from_value(error.take())?;
|
|
||||||
Err(ResponseError::Api {
|
|
||||||
code: dto.code,
|
|
||||||
reason: dto.reason,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => Ok(Self { value }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn decode<'de, D>(&'de self) -> serde_json::Result<D>
|
|
||||||
where
|
|
||||||
D: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
D::deserialize(&self.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn decode_field<'de, D>(&'de self, field: &'static str) -> serde_json::Result<D>
|
|
||||||
where
|
|
||||||
D: Deserialize<'de>,
|
|
||||||
{
|
|
||||||
self.value
|
|
||||||
.get(field)
|
|
||||||
.ok_or_else(|| serde_json::Error::missing_field(field))
|
|
||||||
.and_then(D::deserialize)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn decode_field_with<'de, V, F>(&'de self, field: &'static str, fun: F) -> serde_json::Result<V>
|
|
||||||
where
|
|
||||||
F: FnOnce(&'de serde_json::Value) -> serde_json::Result<V>,
|
|
||||||
{
|
|
||||||
self.value
|
|
||||||
.get(field)
|
|
||||||
.ok_or_else(|| serde_json::Error::missing_field(field))
|
|
||||||
.and_then(fun)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ApiSelectionResponse: Send + Sync + From<ApiResponse> + 'static {
|
#[derive(Debug, Error)]
|
||||||
fn into_inner(self) -> ApiResponse;
|
pub enum Error {
|
||||||
}
|
#[error("Parameter error: {0}")]
|
||||||
|
Parameter(#[from] ParameterError),
|
||||||
pub trait ApiSelection: Send + Sync + 'static {
|
#[error("Network error: {0}")]
|
||||||
type Response: ApiSelectionResponse;
|
Network(#[from] reqwest::Error),
|
||||||
|
#[error("Parsing error: {0}")]
|
||||||
fn raw_value(self) -> &'static str;
|
Parsing(#[from] serde_json::Error),
|
||||||
|
#[error("Api error: {0}")]
|
||||||
fn category() -> &'static str;
|
Api(#[from] ApiError),
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DirectExecutor<C> {
|
|
||||||
key: String,
|
|
||||||
_marker: std::marker::PhantomData<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> DirectExecutor<C> {
|
|
||||||
fn new(key: String) -> Self {
|
|
||||||
Self {
|
|
||||||
key,
|
|
||||||
_marker: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum ApiClientError<C>
|
|
||||||
where
|
|
||||||
C: std::error::Error,
|
|
||||||
{
|
|
||||||
#[error(transparent)]
|
|
||||||
Client(C),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Response(#[from] ResponseError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> ApiClientError<C>
|
|
||||||
where
|
|
||||||
C: std::error::Error,
|
|
||||||
{
|
|
||||||
pub fn api_code(&self) -> Option<u8> {
|
|
||||||
match self {
|
|
||||||
Self::Response(err) => err.api_code(),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ApiRequest<A>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
pub selections: Vec<&'static str>,
|
|
||||||
pub query_items: Vec<(&'static str, String)>,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
phantom: std::marker::PhantomData<A>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A> std::default::Default for ApiRequest<A>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
selections: Vec::default(),
|
|
||||||
query_items: Vec::default(),
|
|
||||||
comment: None,
|
|
||||||
phantom: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A> ApiRequest<A>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
fn add_query_item(&mut self, name: &'static str, value: impl ToString) {
|
|
||||||
if let Some((_, old)) = self.query_items.iter_mut().find(|(n, _)| *n == name) {
|
|
||||||
*old = value.to_string();
|
|
||||||
} else {
|
|
||||||
self.query_items.push((name, value.to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(&self, key: &str, id: Option<&str>) -> String {
|
|
||||||
let mut url = format!("https://api.torn.com/{}/", A::category());
|
|
||||||
|
|
||||||
if let Some(id) = id {
|
|
||||||
write!(url, "{}", id).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
write!(url, "?selections={}&key={}", self.selections.join(","), key).unwrap();
|
|
||||||
|
|
||||||
for (name, value) in &self.query_items {
|
|
||||||
write!(url, "&{name}={value}").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(comment) = &self.comment {
|
|
||||||
write!(url, "&comment={}", comment).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ApiRequestBuilder<A>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
pub request: ApiRequest<A>,
|
|
||||||
pub id: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A> Default for ApiRequestBuilder<A>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
request: Default::default(),
|
|
||||||
id: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<A> ApiRequestBuilder<A>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
#[must_use]
|
|
||||||
pub fn selections(mut self, selections: impl IntoIterator<Item = A>) -> Self {
|
|
||||||
self.request.selections.append(
|
|
||||||
&mut selections
|
|
||||||
.into_iter()
|
|
||||||
.map(ApiSelection::raw_value)
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn from(mut self, from: DateTime<Utc>) -> Self {
|
|
||||||
self.request.add_query_item("from", from.timestamp());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_timestamp(mut self, from: i64) -> Self {
|
|
||||||
self.request.add_query_item("from", from);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn to(mut self, to: DateTime<Utc>) -> Self {
|
|
||||||
self.request.add_query_item("to", to.timestamp());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn to_timestamp(mut self, to: i64) -> Self {
|
|
||||||
self.request.add_query_item("to", to);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn stats_timestamp(mut self, ts: i64) -> Self {
|
|
||||||
self.request.add_query_item("timestamp", ts);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn stats_datetime(mut self, dt: DateTime<Utc>) -> Self {
|
|
||||||
self.request.add_query_item("timestamp", dt.timestamp());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn comment(mut self, comment: String) -> Self {
|
|
||||||
self.request.comment = Some(comment);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn id<I>(mut self, id: I) -> Self
|
|
||||||
where
|
|
||||||
I: ToString,
|
|
||||||
{
|
|
||||||
self.id = Some(id.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) mod tests {
|
|
||||||
use std::sync::Once;
|
|
||||||
|
|
||||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
|
||||||
pub use ::awc::Client;
|
|
||||||
#[cfg(feature = "reqwest")]
|
|
||||||
pub use ::reqwest::Client;
|
|
||||||
|
|
||||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
|
||||||
pub use crate::local::ApiClient as ClientTrait;
|
|
||||||
#[cfg(feature = "reqwest")]
|
|
||||||
pub use crate::send::ApiClient as ClientTrait;
|
|
||||||
|
|
||||||
#[cfg(all(not(feature = "reqwest"), feature = "awc"))]
|
|
||||||
pub use actix_rt::test as async_test;
|
|
||||||
#[cfg(feature = "reqwest")]
|
|
||||||
pub use tokio::test as async_test;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
static INIT: Once = Once::new();
|
|
||||||
|
|
||||||
pub(crate) fn setup() -> String {
|
|
||||||
INIT.call_once(|| {
|
|
||||||
dotenv::dotenv().ok();
|
|
||||||
});
|
|
||||||
std::env::var("APIKEY").expect("api key")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "user")]
|
|
||||||
#[test]
|
|
||||||
fn selection_raw_value() {
|
|
||||||
assert_eq!(user::Selection::Basic.raw_value(), "basic");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(feature = "reqwest", feature = "user"))]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn reqwest() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(feature = "awc", feature = "user"))]
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn awc() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
Client::default().torn_api(key).user(|b| b).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_from_dt() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.from(DateTime::default())
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!("https://api.torn.com/user/?selections=&key=&from=0", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_from_ts() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.from_timestamp(12345)
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"https://api.torn.com/user/?selections=&key=&from=12345",
|
|
||||||
url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_to_dt() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.to(DateTime::default())
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!("https://api.torn.com/user/?selections=&key=&to=0", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_to_ts() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.to_timestamp(12345)
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!("https://api.torn.com/user/?selections=&key=&to=12345", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_timestamp_dt() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.stats_datetime(DateTime::default())
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"https://api.torn.com/user/?selections=&key=×tamp=0",
|
|
||||||
url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_timestamp_ts() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.stats_timestamp(12345)
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"https://api.torn.com/user/?selections=&key=×tamp=12345",
|
|
||||||
url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_duplicate() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.from(DateTime::default())
|
|
||||||
.from_timestamp(12345)
|
|
||||||
.request
|
|
||||||
.url("", None);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"https://api.torn.com/user/?selections=&key=&from=12345",
|
|
||||||
url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn url_builder_many_options() {
|
|
||||||
let url = ApiRequestBuilder::<user::Selection>::default()
|
|
||||||
.from(DateTime::default())
|
|
||||||
.to_timestamp(60)
|
|
||||||
.stats_timestamp(12345)
|
|
||||||
.selections([user::Selection::PersonalStats])
|
|
||||||
.request
|
|
||||||
.url("KEY", Some("1"));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
"https://api.torn.com/user/1?selections=personalstats&key=KEY&from=0&to=60×tamp=12345",
|
|
||||||
url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,282 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::{ApiClientError, ApiRequest, ApiResponse, ApiSelection, DirectExecutor};
|
|
||||||
|
|
||||||
pub struct ApiProvider<'a, C, E>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
E: RequestExecutor<C>,
|
|
||||||
{
|
|
||||||
#[allow(dead_code)]
|
|
||||||
client: &'a C,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
executor: E,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, C, E> ApiProvider<'a, C, E>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
E: RequestExecutor<C>,
|
|
||||||
{
|
|
||||||
pub fn new(client: &'a C, executor: E) -> ApiProvider<'a, C, E> {
|
|
||||||
Self { client, executor }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "user")]
|
|
||||||
pub async fn user<F>(&self, build: F) -> Result<crate::user::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "user")]
|
|
||||||
pub async fn users<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::user::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "faction")]
|
|
||||||
pub async fn faction<F>(&self, build: F) -> Result<crate::faction::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "faction")]
|
|
||||||
pub async fn factions<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::faction::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "market")]
|
|
||||||
pub async fn market<F>(&self, build: F) -> Result<crate::market::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "market")]
|
|
||||||
pub async fn markets<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::market::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "torn")]
|
|
||||||
pub async fn torn<F>(&self, build: F) -> Result<crate::torn::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "torn")]
|
|
||||||
pub async fn torns<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::torn::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "key")]
|
|
||||||
pub async fn key<F>(&self, build: F) -> Result<crate::key::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::key::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::key::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
pub trait RequestExecutor<C>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
{
|
|
||||||
type Error: std::error::Error;
|
|
||||||
|
|
||||||
async fn execute<A>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
id: Option<String>,
|
|
||||||
) -> Result<A::Response, Self::Error>
|
|
||||||
where
|
|
||||||
A: ApiSelection;
|
|
||||||
|
|
||||||
async fn execute_many<A, I>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
ids: Vec<I>,
|
|
||||||
) -> HashMap<I, Result<A::Response, Self::Error>>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
impl<C> RequestExecutor<C> for DirectExecutor<C>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
{
|
|
||||||
type Error = ApiClientError<C::Error>;
|
|
||||||
|
|
||||||
async fn execute<A>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
id: Option<String>,
|
|
||||||
) -> Result<A::Response, Self::Error>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
let url = request.url(&self.key, id.as_deref());
|
|
||||||
|
|
||||||
let value = client.request(url).await.map_err(ApiClientError::Client)?;
|
|
||||||
|
|
||||||
Ok(ApiResponse::from_value(value)?.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_many<A, I>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
ids: Vec<I>,
|
|
||||||
) -> HashMap<I, Result<A::Response, Self::Error>>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq,
|
|
||||||
{
|
|
||||||
let request_ref = &request;
|
|
||||||
let tuples = futures::future::join_all(ids.into_iter().map(|i| async move {
|
|
||||||
let id_string = i.to_string();
|
|
||||||
let url = request_ref.url(&self.key, Some(&id_string));
|
|
||||||
|
|
||||||
let value = client.request(url).await.map_err(ApiClientError::Client);
|
|
||||||
|
|
||||||
(
|
|
||||||
i,
|
|
||||||
value.and_then(|v| {
|
|
||||||
ApiResponse::from_value(v)
|
|
||||||
.map(Into::into)
|
|
||||||
.map_err(Into::into)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
HashMap::from_iter(tuples)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
|
||||||
pub trait ApiClient {
|
|
||||||
type Error: std::error::Error;
|
|
||||||
|
|
||||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error>;
|
|
||||||
|
|
||||||
fn torn_api<S>(&self, key: S) -> ApiProvider<Self, DirectExecutor<Self>>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
S: ToString,
|
|
||||||
{
|
|
||||||
ApiProvider::new(self, DirectExecutor::new(key.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
use serde::Deserialize;
|
|
||||||
use torn_api_macros::ApiCategory;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, ApiCategory)]
|
|
||||||
#[api(category = "market")]
|
|
||||||
pub enum MarketSelection {
|
|
||||||
#[api(type = "Vec<BazaarItem>", field = "bazaar")]
|
|
||||||
Bazaar,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct BazaarItem {
|
|
||||||
pub cost: u64,
|
|
||||||
pub quantity: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use crate::tests::{async_test, setup, Client, ClientTrait};
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn market_bazaar() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.market(|b| b.id(1).selections([MarketSelection::Bazaar]))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
_ = response.bazaar().unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
1
torn-api/src/models.rs
Normal file
1
torn-api/src/models.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/models.rs"));
|
1
torn-api/src/parameters.rs
Normal file
1
torn-api/src/parameters.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/parameters.rs"));
|
104
torn-api/src/request/mod.rs
Normal file
104
torn-api/src/request/mod.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use bon::Builder;
|
||||||
|
use bytes::Bytes;
|
||||||
|
use http::StatusCode;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
executor::Executor,
|
||||||
|
models::{FactionChainsResponse, FactionId},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod models;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ApiRequest<D = ()> {
|
||||||
|
pub disriminant: D,
|
||||||
|
pub path: String,
|
||||||
|
pub parameters: Vec<(&'static str, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<D> ApiRequest<D> {
|
||||||
|
pub fn url(&self) -> String {
|
||||||
|
let mut url = format!("https://api.torn.com/v2{}?", self.path);
|
||||||
|
|
||||||
|
for (name, value) in &self.parameters {
|
||||||
|
url.push_str(&format!("{name}={value}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ApiResponse<D = ()> {
|
||||||
|
pub discriminant: D,
|
||||||
|
pub body: Option<Bytes>,
|
||||||
|
pub status: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoRequest: Send {
|
||||||
|
type Discriminant: Send;
|
||||||
|
type Response: for<'de> serde::Deserialize<'de> + Send;
|
||||||
|
fn into_request(self) -> ApiRequest<Self::Discriminant>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FactionScope<'e, E>(&'e E)
|
||||||
|
where
|
||||||
|
E: Executor;
|
||||||
|
|
||||||
|
impl<E> FactionScope<'_, E>
|
||||||
|
where
|
||||||
|
E: Executor,
|
||||||
|
{
|
||||||
|
pub async fn chains_for_id<S>(
|
||||||
|
&self,
|
||||||
|
id: FactionId,
|
||||||
|
builder: impl FnOnce(
|
||||||
|
FactionChainsRequestBuilder<faction_chains_request_builder::Empty>,
|
||||||
|
) -> FactionChainsRequestBuilder<S>,
|
||||||
|
) -> Result<FactionChainsResponse, E::Error>
|
||||||
|
where
|
||||||
|
S: faction_chains_request_builder::IsComplete,
|
||||||
|
{
|
||||||
|
let r = builder(FactionChainsRequest::with_id(id)).build();
|
||||||
|
|
||||||
|
self.0.fetch(r).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Builder)]
|
||||||
|
#[builder(start_fn = with_id)]
|
||||||
|
pub struct FactionChainsRequest {
|
||||||
|
#[builder(start_fn)]
|
||||||
|
pub id: FactionId,
|
||||||
|
pub limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoRequest for FactionChainsRequest {
|
||||||
|
type Discriminant = FactionId;
|
||||||
|
type Response = FactionChainsResponse;
|
||||||
|
fn into_request(self) -> ApiRequest<Self::Discriminant> {
|
||||||
|
ApiRequest {
|
||||||
|
disriminant: self.id,
|
||||||
|
path: format!("/faction/{}/chains", self.id),
|
||||||
|
parameters: self
|
||||||
|
.limit
|
||||||
|
.into_iter()
|
||||||
|
.map(|l| ("limit", l.to_string()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::executor::ReqwestClient;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_request() {
|
||||||
|
let client = ReqwestClient::new("nAYRXaoqzBAGalWt");
|
||||||
|
|
||||||
|
let r = models::TornItemsForIdsRequest::builder("1".to_owned()).build();
|
||||||
|
client.fetch(r).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
1
torn-api/src/request/models.rs
Normal file
1
torn-api/src/request/models.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/requests.rs"));
|
|
@ -1,12 +0,0 @@
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::send::ApiClient;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl ApiClient for reqwest::Client {
|
|
||||||
type Error = reqwest::Error;
|
|
||||||
|
|
||||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error> {
|
|
||||||
self.get(url).send().await?.json().await
|
|
||||||
}
|
|
||||||
}
|
|
686
torn-api/src/scopes.rs
Normal file
686
torn-api/src/scopes.rs
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/scopes.rs"));
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(super) mod test {
|
||||||
|
use std::{collections::VecDeque, sync::OnceLock, time::Duration};
|
||||||
|
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::executor::ReqwestClient;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
static TICKETS: OnceLock<mpsc::Sender<mpsc::Sender<ReqwestClient>>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub(crate) async fn test_client() -> ReqwestClient {
|
||||||
|
let (tx, mut rx) = mpsc::channel(1);
|
||||||
|
|
||||||
|
let ticket_tx = TICKETS
|
||||||
|
.get_or_init(|| {
|
||||||
|
let (tx, mut rx) = mpsc::channel(1);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut queue = VecDeque::<mpsc::Sender<ReqwestClient>>::new();
|
||||||
|
|
||||||
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
rt.block_on(async move {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
recv_result = rx.recv() => {
|
||||||
|
match recv_result {
|
||||||
|
Some(ticket) => queue.push_back(ticket),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(Duration::from_secs(1)) => {
|
||||||
|
if let Some(next) = queue.pop_front() {
|
||||||
|
next.send(ReqwestClient::new(&std::env::var("API_KEY").unwrap())).await.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tx
|
||||||
|
})
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
ticket_tx.send(tx).await.unwrap();
|
||||||
|
|
||||||
|
rx.recv().await.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_applications() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.applications(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_attacks() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.attacks(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_attacksfull() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.attacksfull(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_balance() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.balance(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_basic() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.basic(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_basic_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.basic_for_id(19.into(), |b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_chain() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.chain(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_chain_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.chain_for_id(19.into(), |b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_chains() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.chains(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn factions_chains_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.chains_for_id(19.into(), |b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_chain_report() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.chainreport(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_chain_report_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.chainreport_for_chain_id(47004769.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_contributors() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.contributors(|b| b.stat(crate::models::FactionStatEnum::Revives))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_crimes() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.crimes(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_crime_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.crime_for_id(468347.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_hof() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.hof(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_hof_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.hof_for_id(19.into(), |b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_members() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.members(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_members_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.members_for_id(19.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_news() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.news(|b| b.cat(crate::models::FactionNewsCategory::Attack))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_ranked_wars() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.rankedwars(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_ranked_war_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.rankedwars_for_id(19.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_ranked_war_report_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope
|
||||||
|
.rankedwarreport_for_id(24424.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_revives() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.revives(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_revives_full() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.revives_full(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_stats() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.stats(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_upgrades() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.upgrades(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_wars() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.wars(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn faction_wars_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.wars_for_id(19.into(), |b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn lookup() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let faction_scope = FactionScope(&client);
|
||||||
|
|
||||||
|
faction_scope.lookup(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_categories() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope.categories(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_posts_for_thread_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope
|
||||||
|
.posts_for_thread_id(16129703.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_thread_for_thread_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope
|
||||||
|
.thread_for_thread_id(16129703.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_threads() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope.threads(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_threads_for_category_ids() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope
|
||||||
|
.threads_for_category_ids("2".to_owned(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_lookup() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope.lookup(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn forum_timestamp() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let forum_scope = ForumScope(&client);
|
||||||
|
|
||||||
|
forum_scope.timestamp(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn market_itemmarket_for_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let market_scope = MarketScope(&client);
|
||||||
|
|
||||||
|
market_scope
|
||||||
|
.itemmarket_for_id(1.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn market_lookup() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let market_scope = MarketScope(&client);
|
||||||
|
|
||||||
|
market_scope.lookup(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn market_timestamp() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let market_scope = MarketScope(&client);
|
||||||
|
|
||||||
|
market_scope.timestamp(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_cars() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope.cars(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_carupgrades() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope.carupgrades(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_races() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope.races(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_race_for_race_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope
|
||||||
|
.race_for_race_id(14650821.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_tracks() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope.tracks(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_lookup() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope.lookup(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn racing_timestamp() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = RacingScope(&client);
|
||||||
|
|
||||||
|
racing_scope.timestamp(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_attacklog() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let racing_scope = TornScope(&client);
|
||||||
|
|
||||||
|
racing_scope
|
||||||
|
.attacklog(|b| b.log("ec987a60a22155cbfb7c1625cbb2092f".to_owned()))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_bounties() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.bounties(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_calendar() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.calendar(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_crimes() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.crimes(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_factionhof() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope
|
||||||
|
.factionhof(|b| b.cat(crate::models::TornFactionHofCategory::Rank))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_factiontree() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.factiontree(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_hof() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope
|
||||||
|
.hof(|b| b.cat(crate::models::TornHofCategory::Offences))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_itemammo() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.itemammo(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_itemmods() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.itemmods(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_items() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.items(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_items_for_ids() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope
|
||||||
|
.items_for_ids("1".to_owned(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_logcategories() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.logcategories(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_logtypes() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.logtypes(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_logtypes_for_log_category_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope
|
||||||
|
.logtypes_for_log_category_id(23.into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_subrcimes_for_crime_id() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope
|
||||||
|
.subcrimes_for_crime_id("3".into(), |b| b)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_lookup() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.lookup(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn torn_timestamp() {
|
||||||
|
let client = test_client().await;
|
||||||
|
|
||||||
|
let torn_scope = TornScope(&client);
|
||||||
|
|
||||||
|
torn_scope.timestamp(|b| b).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,280 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use crate::{ApiClientError, ApiRequest, ApiResponse, ApiSelection, DirectExecutor};
|
|
||||||
|
|
||||||
pub struct ApiProvider<'a, C, E>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
E: RequestExecutor<C>,
|
|
||||||
{
|
|
||||||
client: &'a C,
|
|
||||||
executor: E,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, C, E> ApiProvider<'a, C, E>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
E: RequestExecutor<C>,
|
|
||||||
{
|
|
||||||
pub fn new(client: &'a C, executor: E) -> ApiProvider<'a, C, E> {
|
|
||||||
Self { client, executor }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "user")]
|
|
||||||
pub async fn user<F>(&self, build: F) -> Result<crate::user::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "user")]
|
|
||||||
pub async fn users<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::user::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::user::Selection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq + Send + Sync,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "faction")]
|
|
||||||
pub async fn faction<F>(&self, build: F) -> Result<crate::faction::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "faction")]
|
|
||||||
pub async fn factions<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::faction::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::faction::Selection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq + Send + Sync,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "market")]
|
|
||||||
pub async fn market<F>(&self, build: F) -> Result<crate::market::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "market")]
|
|
||||||
pub async fn markets<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::market::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::market::MarketSelection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq + Send + Sync,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "torn")]
|
|
||||||
pub async fn torn<F>(&self, build: F) -> Result<crate::torn::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "torn")]
|
|
||||||
pub async fn torns<F, L, I>(
|
|
||||||
&self,
|
|
||||||
ids: L,
|
|
||||||
build: F,
|
|
||||||
) -> HashMap<I, Result<crate::torn::Response, E::Error>>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::torn::Selection>,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq + Send + Sync,
|
|
||||||
L: IntoIterator<Item = I>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute_many(self.client, builder.request, Vec::from_iter(ids))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "key")]
|
|
||||||
pub async fn key<F>(&self, build: F) -> Result<crate::key::Response, E::Error>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
crate::ApiRequestBuilder<crate::key::Selection>,
|
|
||||||
) -> crate::ApiRequestBuilder<crate::key::Selection>,
|
|
||||||
{
|
|
||||||
let mut builder = crate::ApiRequestBuilder::default();
|
|
||||||
builder = build(builder);
|
|
||||||
|
|
||||||
self.executor
|
|
||||||
.execute(self.client, builder.request, builder.id)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait RequestExecutor<C>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
{
|
|
||||||
type Error: std::error::Error + Send + Sync;
|
|
||||||
|
|
||||||
async fn execute<A>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
id: Option<String>,
|
|
||||||
) -> Result<A::Response, Self::Error>
|
|
||||||
where
|
|
||||||
A: ApiSelection;
|
|
||||||
|
|
||||||
async fn execute_many<A, I>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
ids: Vec<I>,
|
|
||||||
) -> HashMap<I, Result<A::Response, Self::Error>>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq + Send + Sync;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl<C> RequestExecutor<C> for DirectExecutor<C>
|
|
||||||
where
|
|
||||||
C: ApiClient,
|
|
||||||
{
|
|
||||||
type Error = ApiClientError<C::Error>;
|
|
||||||
|
|
||||||
async fn execute<A>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
id: Option<String>,
|
|
||||||
) -> Result<A::Response, Self::Error>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
{
|
|
||||||
let url = request.url(&self.key, id.as_deref());
|
|
||||||
|
|
||||||
let value = client.request(url).await.map_err(ApiClientError::Client)?;
|
|
||||||
|
|
||||||
Ok(ApiResponse::from_value(value)?.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute_many<A, I>(
|
|
||||||
&self,
|
|
||||||
client: &C,
|
|
||||||
request: ApiRequest<A>,
|
|
||||||
ids: Vec<I>,
|
|
||||||
) -> HashMap<I, Result<A::Response, Self::Error>>
|
|
||||||
where
|
|
||||||
A: ApiSelection,
|
|
||||||
I: ToString + std::hash::Hash + std::cmp::Eq + Send + Sync,
|
|
||||||
{
|
|
||||||
let request_ref = &request;
|
|
||||||
let tuples = futures::future::join_all(ids.into_iter().map(|i| async move {
|
|
||||||
let id_string = i.to_string();
|
|
||||||
let url = request_ref.url(&self.key, Some(&id_string));
|
|
||||||
|
|
||||||
let value = client.request(url).await.map_err(ApiClientError::Client);
|
|
||||||
|
|
||||||
(
|
|
||||||
i,
|
|
||||||
value.and_then(|v| {
|
|
||||||
ApiResponse::from_value(v)
|
|
||||||
.map(Into::into)
|
|
||||||
.map_err(Into::into)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
HashMap::from_iter(tuples)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait ApiClient: Send + Sync {
|
|
||||||
type Error: std::error::Error + Sync + Send;
|
|
||||||
|
|
||||||
async fn request(&self, url: String) -> Result<serde_json::Value, Self::Error>;
|
|
||||||
|
|
||||||
fn torn_api<S>(&self, key: S) -> ApiProvider<Self, DirectExecutor<Self>>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
S: ToString,
|
|
||||||
{
|
|
||||||
ApiProvider::new(self, DirectExecutor::new(key.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,417 +0,0 @@
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{
|
|
||||||
de::{self, MapAccess, Visitor},
|
|
||||||
Deserialize, Deserializer,
|
|
||||||
};
|
|
||||||
|
|
||||||
use torn_api_macros::ApiCategory;
|
|
||||||
|
|
||||||
use crate::{de_util, user};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, ApiCategory)]
|
|
||||||
#[api(category = "torn")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum TornSelection {
|
|
||||||
#[api(
|
|
||||||
field = "competition",
|
|
||||||
with = "decode_competition",
|
|
||||||
type = "Option<Competition>"
|
|
||||||
)]
|
|
||||||
Competition,
|
|
||||||
|
|
||||||
#[api(
|
|
||||||
type = "HashMap<String, TerritoryWar>",
|
|
||||||
with = "decode_territory_wars",
|
|
||||||
field = "territorywars"
|
|
||||||
)]
|
|
||||||
TerritoryWars,
|
|
||||||
|
|
||||||
#[api(type = "HashMap<String, Racket>", field = "rackets")]
|
|
||||||
Rackets,
|
|
||||||
|
|
||||||
#[api(
|
|
||||||
type = "HashMap<String, Territory>",
|
|
||||||
with = "decode_territory",
|
|
||||||
field = "territory"
|
|
||||||
)]
|
|
||||||
Territory,
|
|
||||||
|
|
||||||
#[api(type = "TerritoryWarReport", field = "territorywarreport")]
|
|
||||||
TerritoryWarReport,
|
|
||||||
|
|
||||||
#[api(type = "BTreeMap<i32, Item>", field = "items")]
|
|
||||||
Items,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Selection = TornSelection;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct EliminationLeaderboard {
|
|
||||||
pub position: i16,
|
|
||||||
pub team: user::EliminationTeam,
|
|
||||||
pub score: i16,
|
|
||||||
pub lives: i16,
|
|
||||||
pub participants: Option<i16>,
|
|
||||||
pub wins: Option<i32>,
|
|
||||||
pub losses: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum Competition {
|
|
||||||
Elimination { teams: Vec<EliminationLeaderboard> },
|
|
||||||
Unkown(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_territory_wars<'de, D>(deserializer: D) -> Result<HashMap<String, TerritoryWar>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let map: Option<_> = serde::Deserialize::deserialize(deserializer)?;
|
|
||||||
|
|
||||||
Ok(map.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_competition<'de, D>(deserializer: D) -> Result<Option<Competition>, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct CompetitionVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for CompetitionVisitor {
|
|
||||||
type Value = Option<Competition>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("struct Competition")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserializer.deserialize_map(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
|
||||||
where
|
|
||||||
V: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut name = None;
|
|
||||||
let mut teams = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
|
||||||
match key {
|
|
||||||
"name" => {
|
|
||||||
name = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
"teams" => {
|
|
||||||
teams = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
|
|
||||||
|
|
||||||
match name {
|
|
||||||
"Elimination" => Ok(Some(Competition::Elimination {
|
|
||||||
teams: teams.ok_or_else(|| de::Error::missing_field("teams"))?,
|
|
||||||
})),
|
|
||||||
"" => Ok(None),
|
|
||||||
v => Ok(Some(Competition::Unkown(v.to_owned()))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_option(CompetitionVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct TerritoryWar {
|
|
||||||
pub territory_war_id: i32,
|
|
||||||
pub assaulting_faction: i32,
|
|
||||||
pub defending_faction: i32,
|
|
||||||
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub started: DateTime<Utc>,
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub ends: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Racket {
|
|
||||||
pub name: String,
|
|
||||||
pub level: i16,
|
|
||||||
pub reward: String,
|
|
||||||
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub changed: DateTime<Utc>,
|
|
||||||
|
|
||||||
#[serde(rename = "faction")]
|
|
||||||
pub faction_id: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Territory {
|
|
||||||
pub sector: i16,
|
|
||||||
pub size: i16,
|
|
||||||
pub slots: i16,
|
|
||||||
pub daily_respect: i16,
|
|
||||||
pub faction: i32,
|
|
||||||
|
|
||||||
pub neighbors: Vec<String>,
|
|
||||||
pub war: Option<TerritoryWar>,
|
|
||||||
pub racket: Option<Racket>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_territory<'de, D>(deserializer: D) -> Result<HashMap<String, Territory>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct TerritoryWarReportTerritory {
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum TerritoryWarOutcome {
|
|
||||||
EndWithPeaceTreaty,
|
|
||||||
EndWithDestroyDefense,
|
|
||||||
FailAssault,
|
|
||||||
SuccessAssault,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct TerritoryWarReportWar {
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub start: DateTime<Utc>,
|
|
||||||
#[serde(with = "chrono::serde::ts_seconds")]
|
|
||||||
pub end: DateTime<Utc>,
|
|
||||||
|
|
||||||
pub result: TerritoryWarOutcome,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum TerritoryWarReportRole {
|
|
||||||
Aggressor,
|
|
||||||
Defender,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct TerritoryWarReportFaction {
|
|
||||||
pub name: String,
|
|
||||||
pub score: i32,
|
|
||||||
pub joins: i32,
|
|
||||||
pub clears: i32,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub role: TerritoryWarReportRole,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
pub struct TerritoryWarReport {
|
|
||||||
pub territory: TerritoryWarReportTerritory,
|
|
||||||
pub war: TerritoryWarReportWar,
|
|
||||||
pub factions: HashMap<i32, TerritoryWarReportFaction>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum ItemType {
|
|
||||||
Primary,
|
|
||||||
Secondary,
|
|
||||||
Melee,
|
|
||||||
Temporary,
|
|
||||||
Defensive,
|
|
||||||
Collectible,
|
|
||||||
Medical,
|
|
||||||
Drug,
|
|
||||||
Booster,
|
|
||||||
#[serde(rename = "Energy Drink")]
|
|
||||||
EnergyDrink,
|
|
||||||
Alcohol,
|
|
||||||
Book,
|
|
||||||
Candy,
|
|
||||||
Car,
|
|
||||||
Clothing,
|
|
||||||
Electronic,
|
|
||||||
Enhancer,
|
|
||||||
Flower,
|
|
||||||
Jewelry,
|
|
||||||
Other,
|
|
||||||
Special,
|
|
||||||
#[serde(rename = "Supply Pack")]
|
|
||||||
SupplyPack,
|
|
||||||
Virus,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
//Missing hand to hand because it is not possible as a weapon
|
|
||||||
pub enum WeaponType {
|
|
||||||
Slashing,
|
|
||||||
Rifle,
|
|
||||||
SMG,
|
|
||||||
Piercing,
|
|
||||||
Clubbing,
|
|
||||||
Pistol,
|
|
||||||
#[serde(rename = "Machine gun")]
|
|
||||||
MachineGun,
|
|
||||||
Mechanical,
|
|
||||||
Temporary,
|
|
||||||
Heavy,
|
|
||||||
Shotgun,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct Item<'a> {
|
|
||||||
pub name: String,
|
|
||||||
pub description: String,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_is_none")]
|
|
||||||
pub effect: Option<&'a str>,
|
|
||||||
#[serde(deserialize_with = "de_util::empty_string_is_none")]
|
|
||||||
pub requirement: Option<&'a str>,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub item_type: ItemType,
|
|
||||||
pub weapon_type: Option<WeaponType>,
|
|
||||||
#[serde(deserialize_with = "de_util::zero_is_none")]
|
|
||||||
pub buy_price: Option<u64>,
|
|
||||||
#[serde(deserialize_with = "de_util::zero_is_none")]
|
|
||||||
pub sell_price: Option<u64>,
|
|
||||||
#[serde(deserialize_with = "de_util::zero_is_none")]
|
|
||||||
pub market_value: Option<u64>,
|
|
||||||
#[serde(deserialize_with = "de_util::zero_is_none")]
|
|
||||||
pub circulation: Option<u32>,
|
|
||||||
pub image: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::tests::{async_test, setup, Client, ClientTrait};
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn competition() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.torn(|b| {
|
|
||||||
b.selections([
|
|
||||||
TornSelection::Competition,
|
|
||||||
TornSelection::TerritoryWars,
|
|
||||||
TornSelection::Rackets,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
response.competition().unwrap();
|
|
||||||
response.territory_wars().unwrap();
|
|
||||||
response.rackets().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn territory() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.torn(|b| b.selections([Selection::Territory]).id("NSC"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let territory = response.territory().unwrap();
|
|
||||||
assert!(territory.contains_key("NSC"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn invalid_territory() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.torn(|b| b.selections([Selection::Territory]).id("AAA"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(response.territory().unwrap().is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn territory_war_report() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(&key)
|
|
||||||
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(37403))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
response.territory_war_report().unwrap().war.result,
|
|
||||||
TerritoryWarOutcome::SuccessAssault
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(&key)
|
|
||||||
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(37502))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
response.territory_war_report().unwrap().war.result,
|
|
||||||
TerritoryWarOutcome::FailAssault
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(&key)
|
|
||||||
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(37860))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
response.territory_war_report().unwrap().war.result,
|
|
||||||
TerritoryWarOutcome::EndWithPeaceTreaty
|
|
||||||
);
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(&key)
|
|
||||||
.torn(|b| b.selections([Selection::TerritoryWarReport]).id(23757))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
response.territory_war_report().unwrap().war.result,
|
|
||||||
TerritoryWarOutcome::EndWithDestroyDefense
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn item() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.torn(|b| b.selections([Selection::Items]).id(837))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let item_list = response.items().unwrap();
|
|
||||||
assert!(item_list.contains_key(&837));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,828 +0,0 @@
|
||||||
use serde::{
|
|
||||||
de::{self, MapAccess, Visitor},
|
|
||||||
Deserialize, Deserializer,
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, HashMap},
|
|
||||||
iter::zip,
|
|
||||||
};
|
|
||||||
|
|
||||||
use torn_api_macros::{ApiCategory, IntoOwned};
|
|
||||||
|
|
||||||
use crate::de_util;
|
|
||||||
|
|
||||||
pub use crate::common::{Attack, AttackFull, LastAction, Status};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, ApiCategory)]
|
|
||||||
#[api(category = "user")]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum UserSelection {
|
|
||||||
#[api(type = "Basic", flatten)]
|
|
||||||
Basic,
|
|
||||||
#[api(type = "Profile", flatten)]
|
|
||||||
Profile,
|
|
||||||
#[api(type = "Discord", field = "discord")]
|
|
||||||
Discord,
|
|
||||||
#[api(type = "PersonalStats", field = "personalstats")]
|
|
||||||
PersonalStats,
|
|
||||||
#[api(type = "CriminalRecord", field = "criminalrecord")]
|
|
||||||
Crimes,
|
|
||||||
#[api(type = "BTreeMap<i32, Attack>", field = "attacks")]
|
|
||||||
AttacksFull,
|
|
||||||
#[api(type = "BTreeMap<i32, AttackFull>", field = "attacks")]
|
|
||||||
Attacks,
|
|
||||||
#[api(type = "HashMap<Icon, &str>", field = "icons")]
|
|
||||||
Icons,
|
|
||||||
#[api(type = "Awards<Medals>", flatten)]
|
|
||||||
Medals,
|
|
||||||
#[api(type = "Awards<Honors>", flatten)]
|
|
||||||
Honors,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Selection = UserSelection;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
pub enum Gender {
|
|
||||||
Male,
|
|
||||||
Female,
|
|
||||||
Enby,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned)]
|
|
||||||
pub struct Faction<'a> {
|
|
||||||
pub faction_id: i32,
|
|
||||||
pub faction_name: &'a str,
|
|
||||||
pub days_in_faction: i16,
|
|
||||||
pub position: &'a str,
|
|
||||||
pub faction_tag: Option<&'a str>,
|
|
||||||
pub faction_tag_image: Option<&'a str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_faction<'de, D>(deserializer: D) -> Result<Option<Faction<'de>>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
enum Field {
|
|
||||||
FactionId,
|
|
||||||
FactionName,
|
|
||||||
DaysInFaction,
|
|
||||||
Position,
|
|
||||||
FactionTag,
|
|
||||||
FactionTagImage,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FactionVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for FactionVisitor {
|
|
||||||
type Value = Option<Faction<'de>>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("struct Faction")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
|
||||||
where
|
|
||||||
V: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut faction_id = None;
|
|
||||||
let mut faction_name = None;
|
|
||||||
let mut days_in_faction = None;
|
|
||||||
let mut position = None;
|
|
||||||
let mut faction_tag = None;
|
|
||||||
let mut faction_tag_image = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
|
||||||
match key {
|
|
||||||
Field::FactionId => {
|
|
||||||
faction_id = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::FactionName => {
|
|
||||||
faction_name = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::DaysInFaction => {
|
|
||||||
days_in_faction = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::Position => {
|
|
||||||
position = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::FactionTag => {
|
|
||||||
faction_tag = map.next_value()?;
|
|
||||||
}
|
|
||||||
Field::FactionTagImage => {
|
|
||||||
faction_tag_image = map.next_value()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let faction_id = faction_id.ok_or_else(|| de::Error::missing_field("faction_id"))?;
|
|
||||||
let faction_name =
|
|
||||||
faction_name.ok_or_else(|| de::Error::missing_field("faction_name"))?;
|
|
||||||
let days_in_faction =
|
|
||||||
days_in_faction.ok_or_else(|| de::Error::missing_field("days_in_faction"))?;
|
|
||||||
let position = position.ok_or_else(|| de::Error::missing_field("position"))?;
|
|
||||||
|
|
||||||
if faction_id == 0 {
|
|
||||||
Ok(None)
|
|
||||||
} else {
|
|
||||||
Ok(Some(Faction {
|
|
||||||
faction_id,
|
|
||||||
faction_name,
|
|
||||||
days_in_faction,
|
|
||||||
position,
|
|
||||||
faction_tag,
|
|
||||||
faction_tag_image,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const FIELDS: &[&str] = &[
|
|
||||||
"faction_id",
|
|
||||||
"faction_name",
|
|
||||||
"days_in_faction",
|
|
||||||
"position",
|
|
||||||
"faction_tag",
|
|
||||||
];
|
|
||||||
deserializer.deserialize_struct("Faction", FIELDS, FactionVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned, Deserialize)]
|
|
||||||
pub struct Basic<'a> {
|
|
||||||
pub player_id: i32,
|
|
||||||
pub name: &'a str,
|
|
||||||
pub level: i16,
|
|
||||||
pub gender: Gender,
|
|
||||||
pub status: Status<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, IntoOwned, PartialEq, Eq, Deserialize)]
|
|
||||||
#[into_owned(identity)]
|
|
||||||
pub struct Discord {
|
|
||||||
#[serde(
|
|
||||||
rename = "userID",
|
|
||||||
deserialize_with = "de_util::empty_string_int_option"
|
|
||||||
)]
|
|
||||||
pub user_id: Option<i32>,
|
|
||||||
#[serde(rename = "discordID", deserialize_with = "de_util::string_is_long")]
|
|
||||||
pub discord_id: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct LifeBar {
|
|
||||||
pub current: i16,
|
|
||||||
pub maximum: i16,
|
|
||||||
pub increment: i16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum EliminationTeam2022 {
|
|
||||||
Firestarters,
|
|
||||||
HardBoiled,
|
|
||||||
QuackAddicts,
|
|
||||||
RainMen,
|
|
||||||
TotallyBoned,
|
|
||||||
RawringThunder,
|
|
||||||
DirtyCops,
|
|
||||||
LaughingStock,
|
|
||||||
JeanTherapy,
|
|
||||||
#[serde(rename = "satants-soldiers")]
|
|
||||||
SatansSoldiers,
|
|
||||||
WolfPack,
|
|
||||||
Sleepyheads,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub enum EliminationTeam {
|
|
||||||
Backstabbers,
|
|
||||||
Cheese,
|
|
||||||
DeathsDoor,
|
|
||||||
RegularHumanPeople,
|
|
||||||
FlowerRangers,
|
|
||||||
ReligiousExtremists,
|
|
||||||
Hivemind,
|
|
||||||
CapsLockCrew,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, IntoOwned)]
|
|
||||||
#[into_owned(identity)]
|
|
||||||
pub enum Competition {
|
|
||||||
Elimination {
|
|
||||||
score: i32,
|
|
||||||
attacks: i16,
|
|
||||||
team: EliminationTeam,
|
|
||||||
},
|
|
||||||
DogTags {
|
|
||||||
score: i32,
|
|
||||||
position: Option<i32>,
|
|
||||||
},
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize_comp<'de, D>(deserializer: D) -> Result<Option<Competition>, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
enum Field {
|
|
||||||
Name,
|
|
||||||
Score,
|
|
||||||
Team,
|
|
||||||
Attacks,
|
|
||||||
TeamName,
|
|
||||||
Position,
|
|
||||||
#[serde(other)]
|
|
||||||
Ignore,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
enum CompetitionName {
|
|
||||||
Elimination,
|
|
||||||
#[serde(rename = "Dog Tags")]
|
|
||||||
DogTags,
|
|
||||||
#[serde(other)]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompetitionVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for CompetitionVisitor {
|
|
||||||
type Value = Option<Competition>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("struct Competition")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_none<E>(self) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
deserializer.deserialize_map(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
|
||||||
where
|
|
||||||
V: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut team = None;
|
|
||||||
let mut score = None;
|
|
||||||
let mut attacks = None;
|
|
||||||
let mut name = None;
|
|
||||||
let mut position = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
|
||||||
match key {
|
|
||||||
Field::Name => {
|
|
||||||
name = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::Score => {
|
|
||||||
score = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::Attacks => {
|
|
||||||
attacks = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::Position => {
|
|
||||||
position = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::Team => {
|
|
||||||
let team_raw: &str = map.next_value()?;
|
|
||||||
team = if team_raw.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(match team_raw {
|
|
||||||
"backstabbers" => EliminationTeam::Backstabbers,
|
|
||||||
"cheese" => EliminationTeam::Cheese,
|
|
||||||
"deaths-door" => EliminationTeam::DeathsDoor,
|
|
||||||
"regular-human-people" => EliminationTeam::RegularHumanPeople,
|
|
||||||
"flower-rangers" => EliminationTeam::FlowerRangers,
|
|
||||||
"religious-extremists" => EliminationTeam::ReligiousExtremists,
|
|
||||||
"hivemind" => EliminationTeam::Hivemind,
|
|
||||||
"caps-lock-crew" => EliminationTeam::CapsLockCrew,
|
|
||||||
_ => Err(de::Error::unknown_variant(team_raw, &[]))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = name.ok_or_else(|| de::Error::missing_field("name"))?;
|
|
||||||
|
|
||||||
match name {
|
|
||||||
CompetitionName::Elimination => {
|
|
||||||
if let Some(team) = team {
|
|
||||||
let score = score.ok_or_else(|| de::Error::missing_field("score"))?;
|
|
||||||
let attacks = attacks.ok_or_else(|| de::Error::missing_field("attacks"))?;
|
|
||||||
Ok(Some(Competition::Elimination {
|
|
||||||
team,
|
|
||||||
score,
|
|
||||||
attacks,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CompetitionName::DogTags => {
|
|
||||||
let score = score.ok_or_else(|| de::Error::missing_field("score"))?;
|
|
||||||
let position = position.ok_or_else(|| de::Error::missing_field("position"))?;
|
|
||||||
|
|
||||||
Ok(Some(Competition::DogTags { score, position }))
|
|
||||||
}
|
|
||||||
CompetitionName::Unknown => Ok(Some(Competition::Unknown)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_option(CompetitionVisitor)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, IntoOwned, Deserialize)]
|
|
||||||
pub struct Profile<'a> {
|
|
||||||
pub player_id: i32,
|
|
||||||
pub name: &'a str,
|
|
||||||
pub rank: &'a str,
|
|
||||||
pub level: i16,
|
|
||||||
pub gender: Gender,
|
|
||||||
pub age: i32,
|
|
||||||
|
|
||||||
pub life: LifeBar,
|
|
||||||
pub last_action: LastAction,
|
|
||||||
#[serde(deserialize_with = "deserialize_faction")]
|
|
||||||
pub faction: Option<Faction<'a>>,
|
|
||||||
pub job: EmploymentStatus,
|
|
||||||
pub status: Status<'a>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "deserialize_comp")]
|
|
||||||
pub competition: Option<Competition>,
|
|
||||||
|
|
||||||
#[serde(deserialize_with = "de_util::int_is_bool")]
|
|
||||||
pub revivable: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct PersonalStats {
|
|
||||||
#[serde(rename = "attackswon")]
|
|
||||||
pub attacks_won: i32,
|
|
||||||
#[serde(rename = "attackslost")]
|
|
||||||
pub attacks_lost: i32,
|
|
||||||
#[serde(rename = "defendswon")]
|
|
||||||
pub defends_won: i32,
|
|
||||||
#[serde(rename = "defendslost")]
|
|
||||||
pub defends_lost: i32,
|
|
||||||
#[serde(rename = "statenhancersused")]
|
|
||||||
pub stat_enhancers_used: i32,
|
|
||||||
pub refills: i32,
|
|
||||||
#[serde(rename = "drugsused")]
|
|
||||||
pub drugs_used: i32,
|
|
||||||
#[serde(rename = "xantaken")]
|
|
||||||
pub xanax_taken: i32,
|
|
||||||
#[serde(rename = "lsdtaken")]
|
|
||||||
pub lsd_taken: i32,
|
|
||||||
#[serde(rename = "networth")]
|
|
||||||
pub net_worth: i64,
|
|
||||||
#[serde(rename = "energydrinkused")]
|
|
||||||
pub cans_used: i32,
|
|
||||||
#[serde(rename = "boostersused")]
|
|
||||||
pub boosters_used: i32,
|
|
||||||
pub awards: i16,
|
|
||||||
pub elo: i16,
|
|
||||||
#[serde(rename = "daysbeendonator")]
|
|
||||||
pub days_been_donator: i16,
|
|
||||||
#[serde(rename = "bestdamage")]
|
|
||||||
pub best_damage: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Crimes1 {
|
|
||||||
pub selling_illegal_products: i32,
|
|
||||||
pub theft: i32,
|
|
||||||
pub auto_theft: i32,
|
|
||||||
pub drug_deals: i32,
|
|
||||||
pub computer_crimes: i32,
|
|
||||||
pub murder: i32,
|
|
||||||
pub fraud_crimes: i32,
|
|
||||||
pub other: i32,
|
|
||||||
pub total: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub struct Crimes2 {
|
|
||||||
pub vandalism: i32,
|
|
||||||
pub theft: i32,
|
|
||||||
pub counterfeiting: i32,
|
|
||||||
pub fraud: i32,
|
|
||||||
#[serde(rename = "illicitservices")]
|
|
||||||
pub illicit_services: i32,
|
|
||||||
#[serde(rename = "cybercrime")]
|
|
||||||
pub cyber_crime: i32,
|
|
||||||
pub extortion: i32,
|
|
||||||
#[serde(rename = "illegalproduction")]
|
|
||||||
pub illegal_production: i32,
|
|
||||||
pub total: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
pub enum CriminalRecord {
|
|
||||||
Crimes1(Crimes1),
|
|
||||||
Crimes2(Crimes2),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Icon(i16);
|
|
||||||
|
|
||||||
impl Icon {
|
|
||||||
pub const SUBSCRIBER: Self = Self(4);
|
|
||||||
pub const LEVEL_100: Self = Self(5);
|
|
||||||
pub const GENDER_MALE: Self = Self(6);
|
|
||||||
pub const GENDER_FEMALE: Self = Self(7);
|
|
||||||
pub const MARITAL_STATUS: Self = Self(8);
|
|
||||||
pub const FACTION_MEMBER: Self = Self(9);
|
|
||||||
pub const PLAYER_COMMITTEE: Self = Self(10);
|
|
||||||
pub const STAFF: Self = Self(11);
|
|
||||||
|
|
||||||
pub const COMPANY: Self = Self(27);
|
|
||||||
pub const BANK_INVESTMENT: Self = Self(29);
|
|
||||||
pub const PROPERTY_VAULT: Self = Self(32);
|
|
||||||
pub const DUKE_LOAN: Self = Self(33);
|
|
||||||
|
|
||||||
pub const DRUG_COOLDOWN: Self = Self(53);
|
|
||||||
|
|
||||||
pub const FEDDED: Self = Self(70);
|
|
||||||
pub const TRAVELLING: Self = Self(71);
|
|
||||||
pub const FACTION_LEADER: Self = Self(74);
|
|
||||||
pub const TERRITORY_WAR: Self = Self(75);
|
|
||||||
|
|
||||||
pub const FACTION_RECRUIT: Self = Self(81);
|
|
||||||
pub const STOCK_MARKET: Self = Self(84);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Icon {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct IconVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for IconVisitor {
|
|
||||||
type Value = Icon;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
write!(formatter, "struct Icon")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
||||||
where
|
|
||||||
E: de::Error,
|
|
||||||
{
|
|
||||||
if let Some(suffix) = v.strip_prefix("icon") {
|
|
||||||
Ok(Icon(suffix.parse().map_err(|_e| {
|
|
||||||
de::Error::invalid_value(de::Unexpected::Str(suffix), &"&str \"IconXX\"")
|
|
||||||
})?))
|
|
||||||
} else {
|
|
||||||
Err(de::Error::invalid_value(
|
|
||||||
de::Unexpected::Str(v),
|
|
||||||
&"&str \"iconXX\"",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_str(IconVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum Job {
|
|
||||||
Director,
|
|
||||||
Employee,
|
|
||||||
Education,
|
|
||||||
Army,
|
|
||||||
Law,
|
|
||||||
Casino,
|
|
||||||
Medical,
|
|
||||||
Grocer,
|
|
||||||
#[serde(other)]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Company {
|
|
||||||
PlayerRun {
|
|
||||||
name: String,
|
|
||||||
id: i32,
|
|
||||||
company_type: u8,
|
|
||||||
},
|
|
||||||
CityJob,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Company {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct CompanyVisitor;
|
|
||||||
|
|
||||||
impl<'de> Visitor<'de> for CompanyVisitor {
|
|
||||||
type Value = Company;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("enum Company")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
enum Field {
|
|
||||||
CompanyId,
|
|
||||||
CompanyName,
|
|
||||||
CompanyType,
|
|
||||||
#[serde(other)]
|
|
||||||
Other,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut id = None;
|
|
||||||
let mut name = None;
|
|
||||||
let mut company_type = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key()? {
|
|
||||||
match key {
|
|
||||||
Field::CompanyId => {
|
|
||||||
id = Some(map.next_value()?);
|
|
||||||
if id == Some(0) {
|
|
||||||
return Ok(Company::CityJob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Field::CompanyType => company_type = Some(map.next_value()?),
|
|
||||||
Field::CompanyName => {
|
|
||||||
name = Some(map.next_value()?);
|
|
||||||
}
|
|
||||||
Field::Other => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = id.ok_or_else(|| de::Error::missing_field("company_id"))?;
|
|
||||||
let name = name.ok_or_else(|| de::Error::missing_field("company_name"))?;
|
|
||||||
let company_type =
|
|
||||||
company_type.ok_or_else(|| de::Error::missing_field("company_type"))?;
|
|
||||||
|
|
||||||
Ok(Company::PlayerRun {
|
|
||||||
name,
|
|
||||||
id,
|
|
||||||
company_type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_map(CompanyVisitor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
|
||||||
pub struct EmploymentStatus {
|
|
||||||
pub job: Job,
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub company: Company,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Award {
|
|
||||||
pub award_time: chrono::DateTime<chrono::Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AwardMarker {
|
|
||||||
fn award_key() -> &'static str;
|
|
||||||
fn time_key() -> &'static str;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Awards<T>
|
|
||||||
where
|
|
||||||
T: AwardMarker,
|
|
||||||
{
|
|
||||||
pub inner: BTreeMap<i32, Award>,
|
|
||||||
marker: std::marker::PhantomData<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> std::ops::Deref for Awards<T>
|
|
||||||
where
|
|
||||||
T: AwardMarker,
|
|
||||||
{
|
|
||||||
type Target = BTreeMap<i32, Award>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> std::ops::DerefMut for Awards<T>
|
|
||||||
where
|
|
||||||
T: AwardMarker,
|
|
||||||
{
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Awards<T>
|
|
||||||
where
|
|
||||||
T: AwardMarker,
|
|
||||||
{
|
|
||||||
pub fn into_inner(self) -> BTreeMap<i32, Award> {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Medals;
|
|
||||||
|
|
||||||
impl AwardMarker for Medals {
|
|
||||||
#[inline(always)]
|
|
||||||
fn award_key() -> &'static str {
|
|
||||||
"medals_awarded"
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn time_key() -> &'static str {
|
|
||||||
"medals_time"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Honors;
|
|
||||||
|
|
||||||
impl AwardMarker for Honors {
|
|
||||||
#[inline(always)]
|
|
||||||
fn award_key() -> &'static str {
|
|
||||||
"honors_awarded"
|
|
||||||
}
|
|
||||||
#[inline(always)]
|
|
||||||
fn time_key() -> &'static str {
|
|
||||||
"honors_time"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de, T> Deserialize<'de> for Awards<T>
|
|
||||||
where
|
|
||||||
T: AwardMarker,
|
|
||||||
{
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: Deserializer<'de>,
|
|
||||||
{
|
|
||||||
struct AwardVisitor<T>(std::marker::PhantomData<T>)
|
|
||||||
where
|
|
||||||
T: AwardMarker;
|
|
||||||
|
|
||||||
impl<'de, T> Visitor<'de> for AwardVisitor<T>
|
|
||||||
where
|
|
||||||
T: AwardMarker,
|
|
||||||
{
|
|
||||||
type Value = Awards<T>;
|
|
||||||
|
|
||||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
formatter.write_str("struct awards")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
||||||
where
|
|
||||||
A: MapAccess<'de>,
|
|
||||||
{
|
|
||||||
let mut awards: Option<Vec<i32>> = None;
|
|
||||||
let mut times: Option<Vec<_>> = None;
|
|
||||||
|
|
||||||
while let Some(key) = map.next_key::<&'de str>()? {
|
|
||||||
if key == T::award_key() {
|
|
||||||
awards = map.next_value()?;
|
|
||||||
} else if key == T::time_key() {
|
|
||||||
times = map.next_value()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let awards = awards.ok_or_else(|| de::Error::missing_field(T::award_key()))?;
|
|
||||||
let times = times.ok_or_else(|| de::Error::missing_field(T::time_key()))?;
|
|
||||||
|
|
||||||
Ok(Awards {
|
|
||||||
inner: zip(
|
|
||||||
awards,
|
|
||||||
times.into_iter().map(|t| Award {
|
|
||||||
award_time: chrono::DateTime::from_timestamp(t, 0).unwrap_or_default(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.collect(),
|
|
||||||
marker: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deserializer.deserialize_map(AwardVisitor(Default::default()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::tests::{async_test, setup, Client, ClientTrait};
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn user() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.user(|b| {
|
|
||||||
b.selections([
|
|
||||||
Selection::Basic,
|
|
||||||
Selection::Discord,
|
|
||||||
Selection::Profile,
|
|
||||||
Selection::PersonalStats,
|
|
||||||
Selection::Crimes,
|
|
||||||
Selection::Attacks,
|
|
||||||
Selection::Medals,
|
|
||||||
Selection::Honors,
|
|
||||||
])
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
response.basic().unwrap();
|
|
||||||
response.discord().unwrap();
|
|
||||||
response.profile().unwrap();
|
|
||||||
response.personal_stats().unwrap();
|
|
||||||
response.crimes().unwrap();
|
|
||||||
response.attacks().unwrap();
|
|
||||||
response.attacks_full().unwrap();
|
|
||||||
response.medals().unwrap();
|
|
||||||
response.honors().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn not_in_faction() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.user(|b| b.id(28).selections([Selection::Profile]))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let faction = response.profile().unwrap().faction;
|
|
||||||
|
|
||||||
assert!(faction.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn bulk() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.users([1, 2111649, 374272176892674048i64], |b| {
|
|
||||||
b.selections([Selection::Basic])
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
response.get(&1).as_ref().unwrap().as_ref().unwrap();
|
|
||||||
response.get(&2111649).as_ref().unwrap().as_ref().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn discord() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.user(|b| b.id(374272176892674048i64).selections([Selection::Basic]))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(response.basic().unwrap().player_id, 2111649);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_test]
|
|
||||||
async fn fedded() {
|
|
||||||
let key = setup();
|
|
||||||
|
|
||||||
let response = Client::default()
|
|
||||||
.torn_api(key)
|
|
||||||
.user(|b| b.id(1900654).selections([Selection::Icons]))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let icons = response.icons().unwrap();
|
|
||||||
|
|
||||||
assert!(icons.contains_key(&Icon::FEDDED))
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue