You've already forked AstralRinth
forked from didirus/AstralRinth
Create schema for the API (#28)
* feat(schema): add basic structs for schema * feat(schema): implement base62 id parsing * docs(schema): add documentation for schema structs fix(schema): prevent integer overflow in base62 decoding * refactor(schema): move ids into submodules, reexport from ids mod * feat(schema): add random generation of base62 ids style: run rustfmt
This commit is contained in:
@@ -18,6 +18,8 @@ meilisearch-sdk = "0.1.4"
|
|||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = {version="1.0", features=["derive"]}
|
serde = {version="1.0", features=["derive"]}
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
rand = "0.7"
|
||||||
|
|
||||||
dotenv = "0.15"
|
dotenv = "0.15"
|
||||||
log = "0.4.8"
|
log = "0.4.8"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// An error returned by the API
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct ApiError<'a> {
|
pub struct ApiError<'a> {
|
||||||
pub error: &'a str,
|
pub error: &'a str,
|
||||||
|
|||||||
180
src/models/ids.rs
Normal file
180
src/models/ids.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use super::mods::{ModId, VersionId};
|
||||||
|
pub use super::teams::{TeamId, UserId};
|
||||||
|
|
||||||
|
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||||
|
/// long when encoded as base62.
|
||||||
|
///
|
||||||
|
/// Uses `rand`'s thread rng on every call.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||||
|
/// can only represent up to 11 character base62 strings
|
||||||
|
pub fn random_base62(n: usize) -> u64 {
|
||||||
|
use rand::Rng;
|
||||||
|
assert!(n > 0 && n <= 11);
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
// gen_range is [low, high): max value is `MULTIPLES[n] - 1`,
|
||||||
|
// which is n characters long when encoded
|
||||||
|
rng.gen_range(MULTIPLES[n - 1], MULTIPLES[n])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a random 64 bit integer that is exactly `n` characters
|
||||||
|
/// long when encoded as base62, using the given rng.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
///
|
||||||
|
/// This method panics if `n` is 0 or greater than 11, since a `u64`
|
||||||
|
/// can only represent up to 11 character base62 strings
|
||||||
|
pub fn random_base62_rng<R: rand::RngCore>(rng: &mut R, n: usize) -> u64 {
|
||||||
|
use rand::Rng;
|
||||||
|
assert!(n > 0 && n <= 11);
|
||||||
|
rng.gen_range(MULTIPLES[n - 1], MULTIPLES[n])
|
||||||
|
}
|
||||||
|
|
||||||
|
const MULTIPLES: [u64; 12] = [
|
||||||
|
1,
|
||||||
|
62,
|
||||||
|
62 * 62,
|
||||||
|
62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||||
|
62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62 * 62,
|
||||||
|
std::u64::MAX,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// An ID encoded as base62 for use in the API.
|
||||||
|
///
|
||||||
|
/// All ids should be random and encode to 8-10 character base62 strings,
|
||||||
|
/// to avoid enumeration and other attacks.
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Base62Id(pub u64);
|
||||||
|
|
||||||
|
/// An error decoding a number from base62.
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum DecodingError {
|
||||||
|
/// Encountered a non base62 character in base62 string
|
||||||
|
#[error("Invalid character `{0:?}` in base62 encoding")]
|
||||||
|
InvalidBase62(char),
|
||||||
|
/// Encountered integer overflow when decoding a base62 id.
|
||||||
|
#[error("Base62 decoding overflowed")]
|
||||||
|
Overflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! from_base62id {
|
||||||
|
($($struct:ty, $con:expr;)+) => {
|
||||||
|
$(
|
||||||
|
impl From<Base62Id> for $struct {
|
||||||
|
fn from(id: Base62Id) -> $struct {
|
||||||
|
$con(id.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<$struct> for Base62Id {
|
||||||
|
fn from(id: $struct) -> Base62Id {
|
||||||
|
Base62Id(id.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)+
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
from_base62id! {
|
||||||
|
ModId, ModId;
|
||||||
|
UserId, UserId;
|
||||||
|
VersionId, VersionId;
|
||||||
|
TeamId, TeamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod base62_impl {
|
||||||
|
use serde::de::{self, Deserializer, Visitor};
|
||||||
|
use serde::ser::Serializer;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::{Base62Id, DecodingError};
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Base62Id {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct Base62Visitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for Base62Visitor {
|
||||||
|
type Value = Base62Id;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
|
formatter.write_str("a base62 string id")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, string: &str) -> Result<Base62Id, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
parse_base62(string).map(Base62Id).map_err(E::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_str(Base62Visitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Base62Id {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&to_base62(self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE62_CHARS: [u8; 62] = [
|
||||||
|
b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E',
|
||||||
|
b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T',
|
||||||
|
b'U', b'V', b'W', b'X', b'Y', b'Z', b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i',
|
||||||
|
b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x',
|
||||||
|
b'y', b'z',
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn to_base62(mut num: u64) -> String {
|
||||||
|
let length = (num as f64).log(62.0).ceil() as usize;
|
||||||
|
let mut output = String::with_capacity(length);
|
||||||
|
|
||||||
|
while num > 0 {
|
||||||
|
// Could be done more efficiently, but requires byte
|
||||||
|
// manipulation of strings & Vec<u8> -> String conversion
|
||||||
|
output.insert(0, BASE62_CHARS[(num % 62) as usize] as char);
|
||||||
|
num /= 62;
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_base62(string: &str) -> Result<u64, DecodingError> {
|
||||||
|
let mut num: u64 = 0;
|
||||||
|
for c in string.chars().rev() {
|
||||||
|
let next_digit;
|
||||||
|
if c.is_ascii_digit() {
|
||||||
|
next_digit = (c as u8 - b'0') as u64;
|
||||||
|
} else if c.is_ascii_uppercase() {
|
||||||
|
next_digit = 10 + (c as u8 - b'A') as u64;
|
||||||
|
} else if c.is_ascii_lowercase() {
|
||||||
|
next_digit = 36 + (c as u8 - b'a') as u64;
|
||||||
|
} else {
|
||||||
|
return Err(DecodingError::InvalidBase62(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want this panicing or wrapping on integer overflow
|
||||||
|
if let Some(n) = num.checked_mul(62).and_then(|n| n.checked_add(next_digit)) {
|
||||||
|
num = n;
|
||||||
|
} else {
|
||||||
|
return Err(DecodingError::Overflow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,4 @@
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod ids;
|
||||||
pub mod mods;
|
pub mod mods;
|
||||||
|
pub mod teams;
|
||||||
|
|||||||
@@ -1,5 +1,113 @@
|
|||||||
|
use super::ids::Base62Id;
|
||||||
|
use super::teams::Team;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The ID of a specific mod, encoded as base62 for usage in the API
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "Base62Id")]
|
||||||
|
#[serde(into = "Base62Id")]
|
||||||
|
pub struct ModId(pub u64);
|
||||||
|
|
||||||
|
/// The ID of a specific version of a mod
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "Base62Id")]
|
||||||
|
#[serde(into = "Base62Id")]
|
||||||
|
pub struct VersionId(pub u64);
|
||||||
|
|
||||||
|
/// A mod returned from the API
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Mod {
|
||||||
|
/// The ID of the mod, encoded as a base62 string.
|
||||||
|
pub id: ModId,
|
||||||
|
// TODO: send partial team structure to reduce requests, but avoid sending
|
||||||
|
// unnecessary info
|
||||||
|
/// The team of people that has ownership of this mod.
|
||||||
|
pub team: Team,
|
||||||
|
|
||||||
|
/// The title or name of the mod.
|
||||||
|
pub title: String,
|
||||||
|
/// A short description of the mod.
|
||||||
|
pub description: String,
|
||||||
|
/// The date at which the mod was first published.
|
||||||
|
pub published: DateTime<Utc>,
|
||||||
|
|
||||||
|
/// The total number of downloads the mod has had.
|
||||||
|
pub downloads: u32,
|
||||||
|
/// A list of the categories that the mod is in.
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
/// A list of ids for versions of the mod.
|
||||||
|
pub versions: Vec<VersionId>,
|
||||||
|
|
||||||
|
/// The latest version of the mod.
|
||||||
|
pub latest_version: Version,
|
||||||
|
|
||||||
|
/// An optional link to where to submit bugs or issues with the mod.
|
||||||
|
pub issues_url: Option<String>,
|
||||||
|
/// An optional link to the source code for the mod.
|
||||||
|
pub source_url: Option<String>,
|
||||||
|
/// An optional link to the mod's wiki page or other relevant information.
|
||||||
|
pub wiki_url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A specific version of a mod
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Version {
|
||||||
|
/// The ID of the version, encoded as a base62 string.
|
||||||
|
pub id: VersionId,
|
||||||
|
/// The ID of the mod this version is for.
|
||||||
|
pub mod_id: ModId,
|
||||||
|
|
||||||
|
/// The name of this version
|
||||||
|
pub name: String,
|
||||||
|
/// A link to the changelog for this version of the mod.
|
||||||
|
pub changelog_url: Option<String>,
|
||||||
|
/// The date that this version was published.
|
||||||
|
pub date_published: DateTime<Utc>,
|
||||||
|
/// The number of downloads this specific version has had.
|
||||||
|
pub downloads: u32,
|
||||||
|
/// The type of the release - `Alpha`, `Beta`, or `Release`.
|
||||||
|
pub version_type: VersionType,
|
||||||
|
|
||||||
|
/// A list of files available for download for this version.
|
||||||
|
pub files: Vec<VersionFile>,
|
||||||
|
/// A list of mods that this version depends on.
|
||||||
|
pub dependencies: Vec<ModId>,
|
||||||
|
/// A list of versions of Minecraft that this version of the mod supports.
|
||||||
|
pub game_versions: Vec<GameVersion>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single mod file, with a url for the file and the file's hash
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct VersionFile {
|
||||||
|
/// A list of hashes of the file
|
||||||
|
pub hashes: Vec<FileHash>,
|
||||||
|
/// A direct link to the file for downloading it.
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hash of a mod's file
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct FileHash {
|
||||||
|
// TODO: decide specific algorithms
|
||||||
|
/// The hashing algorithm used for this hash; could be "md5", "sha1", etc
|
||||||
|
pub algorithm: String,
|
||||||
|
/// The file hash, using the specified algorithm
|
||||||
|
pub hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub enum VersionType {
|
||||||
|
Release,
|
||||||
|
Beta,
|
||||||
|
Alpha,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A specific version of Minecraft
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct GameVersion(pub String);
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct SearchRequest {
|
pub struct SearchRequest {
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
|
|||||||
33
src/models/teams.rs
Normal file
33
src/models/teams.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use super::ids::Base62Id;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The ID of a specific user, encoded as base62 for usage in the API
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "Base62Id")]
|
||||||
|
#[serde(into = "Base62Id")]
|
||||||
|
pub struct UserId(pub u64);
|
||||||
|
|
||||||
|
/// The ID of a team
|
||||||
|
#[derive(Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(from = "Base62Id")]
|
||||||
|
#[serde(into = "Base62Id")]
|
||||||
|
pub struct TeamId(pub u64);
|
||||||
|
|
||||||
|
// TODO: permissions, role names, etc
|
||||||
|
/// A team of users who control a mod
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct Team {
|
||||||
|
/// The id of the team
|
||||||
|
pub id: TeamId,
|
||||||
|
/// A list of the members of the team
|
||||||
|
pub members: Vec<TeamMember>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A member of a team
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct TeamMember {
|
||||||
|
/// The ID of the user associated with the member
|
||||||
|
pub user_id: UserId,
|
||||||
|
/// The name of the user
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user