{
+ 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::>()
+ .join(", ")
+ )
+ }
+ }
+}
diff --git a/torn-api-codegen/src/model/parameter.rs b/torn-api-codegen/src/model/parameter.rs
new file mode 100644
index 0000000..efa076e
--- /dev/null
+++ b/torn-api-codegen/src/model/parameter.rs
@@ -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 {
+ pub default: Option
,
+ pub minimum: Option
,
+ pub maximum: Option
,
+}
+
+#[derive(Debug, Clone)]
+pub enum ParameterType {
+ I32 {
+ options: ParameterOptions,
+ },
+ String,
+ Boolean,
+ Enum {
+ options: ParameterOptions,
+ r#type: Enum,
+ },
+ Schema {
+ type_name: String,
+ },
+ Array {
+ items: Box,
+ },
+}
+
+impl ParameterType {
+ pub fn from_schema(name: &str, schema: &OpenApiParameterSchema) -> Option {
+ 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,
+ pub r#type: ParameterType,
+ pub required: bool,
+ pub location: ParameterLocation,
+}
+
+impl Parameter {
+ pub fn from_schema(name: &str, schema: &OpenApiParameter) -> Option {
+ 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 {
+ 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 {
+ if inner > #max || inner < #min {
+ Err(crate::ParameterError::OutOfRange { value: inner, name: #name_raw })
+ } else {
+ Ok(Self(inner))
+ }
+ }
+ }
+
+ impl TryFrom for #name {
+ type Error = crate::ParameterError;
+ fn try_from(inner: i32) -> Result {
+ 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::>()
+ .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(", ")
+ )
+ }
+ }
+}
diff --git a/torn-api-codegen/src/model/path.rs b/torn-api-codegen/src/model/path.rs
new file mode 100644
index 0000000..59f499e
--- /dev/null
+++ b/torn-api-codegen/src/model/path.rs
@@ -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,
+ pub name: String,
+ pub summary: Option,
+ pub description: String,
+ pub parameters: Vec,
+ pub response: PathResponse,
+}
+
+impl Path {
+ pub fn from_schema(
+ path: &str,
+ schema: &OpenApiPath,
+ parameters: &IndexMap<&str, OpenApiParameter>,
+ ) -> Option {
+ 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 {
+ 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 {
+ #[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 {
+ 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(
+ &self,
+ #(#extra_args)*
+ builder: impl FnOnce(
+ #builder_path<#builder_mod_path::Empty>
+ ) -> #builder_path,
+ ) -> 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,
+ elements: Vec,
+}
+
+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 {
+ 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::>()
+ .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::>()
+ .join(", ")
+ )
+ }
+ }
+}
diff --git a/torn-api-codegen/src/model/scope.rs b/torn-api-codegen/src/model/scope.rs
new file mode 100644
index 0000000..2aa57bc
--- /dev/null
+++ b/torn-api-codegen/src/model/scope.rs
@@ -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,
+}
+
+impl Scope {
+ pub fn from_paths(paths: Vec) -> Vec {
+ 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 {
+ 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)*
+ }
+ })
+ }
+}
diff --git a/torn-api-codegen/src/model/union.rs b/torn-api-codegen/src/model/union.rs
new file mode 100644
index 0000000..4e2575a
--- /dev/null
+++ b/torn-api-codegen/src/model/union.rs
@@ -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,
+}
+
+impl Union {
+ pub fn from_schema(name: &str, schema: &OpenApiResponseBody) -> Option {
+ 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 {
+ 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 {
+ ::deserialize(&self.0)
+ }
+ });
+ }
+
+ Some(quote! {
+ #[derive(Debug, Clone, serde::Deserialize)]
+ pub struct #name(serde_json::Value);
+
+ impl #name {
+ #(#variants)*
+ }
+ })
+ }
+}
diff --git a/torn-api-codegen/src/openapi/mod.rs b/torn-api-codegen/src/openapi/mod.rs
new file mode 100644
index 0000000..c1ab891
--- /dev/null
+++ b/torn-api-codegen/src/openapi/mod.rs
@@ -0,0 +1,4 @@
+pub mod parameter;
+pub mod path;
+pub mod schema;
+pub mod r#type;
diff --git a/torn-api-codegen/src/openapi/parameter.rs b/torn-api-codegen/src/openapi/parameter.rs
new file mode 100644
index 0000000..0b341d2
--- /dev/null
+++ b/torn-api-codegen/src/openapi/parameter.rs
@@ -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>,
+ pub format: Option<&'a str>,
+ pub default: Option>,
+ pub maximum: Option,
+ pub minimum: Option,
+ pub items: Option>>,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct OpenApiParameter<'a> {
+ pub name: &'a str,
+ pub description: Option>,
+ pub r#in: ParameterLocation,
+ pub required: bool,
+ #[serde(borrow)]
+ pub schema: OpenApiParameterSchema<'a>,
+}
diff --git a/torn-api-codegen/src/openapi/path.rs b/torn-api-codegen/src/openapi/path.rs
new file mode 100644
index 0000000..23c5767
--- /dev/null
+++ b/torn-api-codegen/src/openapi/path.rs
@@ -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>,
+ },
+}
+
+fn deserialize_response_body<'de, D>(deserializer: D) -> Result, 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>,
+ pub description: Cow<'a, str>,
+ #[serde(borrow, default)]
+ pub parameters: Vec>,
+ #[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>,
+}
diff --git a/torn-api-codegen/src/openapi/schema.rs b/torn-api-codegen/src/openapi/schema.rs
new file mode 100644
index 0000000..2450f01
--- /dev/null
+++ b/torn-api-codegen/src/openapi/schema.rs
@@ -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 {
+ let s = include_str!("../../openapi.json");
+
+ serde_json::from_str(s)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn read() {
+ OpenApiSchema::read().unwrap();
+ }
+}
diff --git a/torn-api-codegen/src/openapi/type.rs b/torn-api-codegen/src/openapi/type.rs
new file mode 100644
index 0000000..44f31e9
--- /dev/null
+++ b/torn-api-codegen/src/openapi/type.rs
@@ -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),
+ #[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>,
+
+ pub r#type: Option<&'a str>,
+ pub format: Option<&'a str>,
+
+ #[serde(rename = "$ref")]
+ pub ref_path: Option<&'a str>,
+
+ pub one_of: Option>>,
+ pub all_of: Option>>,
+
+ pub required: Option>,
+ #[serde(borrow)]
+ pub properties: Option>>,
+
+ pub items: Option>>,
+ pub r#enum: Option>,
+}
+
+#[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"]));
+ }
+}
diff --git a/torn-api/Cargo.toml b/torn-api/Cargo.toml
index 9bfe952..c48651b 100644
--- a/torn-api/Cargo.toml
+++ b/torn-api/Cargo.toml
@@ -1,51 +1,27 @@
[package]
name = "torn-api"
-version = "0.7.5"
-edition = "2021"
-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 = []
+version = "1.0.0"
+edition = "2024"
[dependencies]
-serde = { version = "1", features = [ "derive" ] }
-serde_json = "1"
-chrono = { version = "0.4.31", features = [ "serde" ], default-features = false }
-async-trait = "0.1"
-thiserror = "1"
-futures = "0.3"
-
-reqwest = { version = "0.12", default-features = false, features = [ "json" ], optional = true }
-awc = { version = "3", default-features = false, optional = true }
-rust_decimal = { version = "1", default-features = false, optional = true, features = [ "serde" ] }
-
-torn-api-macros = { path = "../torn-api-macros", version = "0.3.1" }
+serde = { workspace = true, features = ["derive"] }
+serde_repr = "0.1"
+serde_json = { workspace = true }
+bon = "3.6"
+bytes = "1"
+http = "1"
+reqwest = { version = "0.12", default-features = false, features = [
+ "rustls-tls",
+ "json",
+ "brotli",
+] }
+thiserror = "2"
[dev-dependencies]
-actix-rt = { version = "2.7.0" }
-dotenv = "0.15.0"
-tokio = { version = "1.20.1", features = ["test-util", "rt", "macros"] }
-tokio-test = "0.4.2"
-reqwest = { version = "0.12", default-features = true }
-awc = { version = "3", features = [ "rustls" ] }
-criterion = "0.5"
+tokio = { version = "1", features = ["full"] }
+
+[build-dependencies]
+torn-api-codegen = { path = "../torn-api-codegen" }
+syn = { workspace = true, features = ["parsing"] }
+proc-macro2 = { workspace = true }
+prettyplease = "0.2"
diff --git a/torn-api/benches/deserialisation_benchmark.rs b/torn-api/benches/deserialisation_benchmark.rs
deleted file mode 100644
index 0eb52b0..0000000
--- a/torn-api/benches/deserialisation_benchmark.rs
+++ /dev/null
@@ -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);
diff --git a/torn-api/build.rs b/torn-api/build.rs
new file mode 100644
index 0000000..136a95e
--- /dev/null
+++ b/torn-api/build.rs
@@ -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();
+}
diff --git a/torn-api/src/awc.rs b/torn-api/src/awc.rs
deleted file mode 100644
index 7e16904..0000000
--- a/torn-api/src/awc.rs
+++ /dev/null
@@ -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 {
- self.get(url).send().await?.json().await.map_err(Into::into)
- }
-}
diff --git a/torn-api/src/common.rs b/torn-api/src/common.rs
deleted file mode 100644
index e461dd7..0000000
--- a/torn-api/src/common.rs
+++ /dev/null
@@ -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,
- 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>,
-}
-
-#[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,
- #[serde(with = "ts_seconds")]
- pub timestamp_ended: DateTime,
-
- #[serde(deserialize_with = "de_util::empty_string_int_option")]
- pub attacker_id: Option,
- #[serde(deserialize_with = "de_util::empty_string_int_option")]
- pub attacker_faction: Option,
- pub defender_id: i32,
- #[serde(deserialize_with = "de_util::empty_string_int_option")]
- pub defender_faction: Option,
- 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,
- #[serde(with = "ts_seconds")]
- pub timestamp_ended: DateTime,
-
- #[serde(deserialize_with = "de_util::empty_string_int_option")]
- pub attacker_id: Option,
- #[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,
- #[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,
- #[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,
-}
diff --git a/torn-api/src/de_util.rs b/torn-api/src/de_util.rs
deleted file mode 100644
index 939cfa6..0000000
--- a/torn-api/src/de_util.rs
+++ /dev/null
@@ -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