mirror of
https://github.com/modrinth/code.git
synced 2026-07-31 21:26:40 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3316ba5c1 | ||
|
|
ddbf3d2b74 | ||
|
|
4ee470bc43 |
@@ -1059,15 +1059,11 @@ const currentGameVersion = computed(() => {
|
||||
})
|
||||
|
||||
const possibleGameVersions = computed(() => {
|
||||
return versions.value
|
||||
.filter((x) => !currentPlatform.value || x.loaders.includes(currentPlatform.value))
|
||||
.flatMap((x) => x.game_versions)
|
||||
return versionsV3.value?.available_game_versions || []
|
||||
})
|
||||
|
||||
const possiblePlatforms = computed(() => {
|
||||
return versions.value
|
||||
.filter((x) => !currentGameVersion.value || x.game_versions.includes(currentGameVersion.value))
|
||||
.flatMap((x) => x.loaders)
|
||||
return versionsV3.value?.available_loaders || []
|
||||
})
|
||||
|
||||
const currentPlatform = computed(() => {
|
||||
@@ -1414,29 +1410,11 @@ const filteredVersions = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
const filteredRelease = computed(() => {
|
||||
return filteredVersions.value.find((x) => x.version_type === 'release')
|
||||
})
|
||||
const filteredRelease = computed(() => versionsV3.value?.latest_versions?.release || null)
|
||||
|
||||
const filteredBeta = computed(() => {
|
||||
return filteredVersions.value.find(
|
||||
(x) =>
|
||||
x.version_type === 'beta' &&
|
||||
(!filteredRelease.value ||
|
||||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))),
|
||||
)
|
||||
})
|
||||
const filteredBeta = computed(() => versionsV3.value?.latest_versions?.beta || null)
|
||||
|
||||
const filteredAlpha = computed(() => {
|
||||
return filteredVersions.value.find(
|
||||
(x) =>
|
||||
x.version_type === 'alpha' &&
|
||||
(!filteredRelease.value ||
|
||||
dayjs(x.date_published).isAfter(dayjs(filteredRelease.value.date_published))) &&
|
||||
(!filteredBeta.value ||
|
||||
dayjs(x.date_published).isAfter(dayjs(filteredBeta.value.date_published))),
|
||||
)
|
||||
})
|
||||
const filteredAlpha = computed(() => versionsV3.value?.latest_versions?.alpha || null)
|
||||
|
||||
const displayCollectionsSearch = ref('')
|
||||
const collections = computed(() =>
|
||||
@@ -1472,14 +1450,12 @@ let project,
|
||||
dependencies,
|
||||
versions,
|
||||
versionsV3,
|
||||
resetVersionsV2,
|
||||
organization,
|
||||
resetOrganization,
|
||||
projectV2Error,
|
||||
projectV3Error,
|
||||
membersError,
|
||||
dependenciesError,
|
||||
versionsError,
|
||||
versionsV3Error,
|
||||
resetVersionsV3
|
||||
try {
|
||||
@@ -1488,7 +1464,6 @@ try {
|
||||
{ data: projectV3, error: projectV3Error, refresh: resetProjectV3 },
|
||||
{ data: allMembers, error: membersError, refresh: resetMembers },
|
||||
{ data: dependencies, error: dependenciesError },
|
||||
{ data: versions, error: versionsError, refresh: resetVersionsV2 },
|
||||
{ data: versionsV3, error: versionsV3Error, refresh: resetVersionsV3 },
|
||||
{ data: organization, refresh: resetOrganization },
|
||||
] = await Promise.all([
|
||||
@@ -1528,9 +1503,6 @@ try {
|
||||
useAsyncData(`project/${projectId.value}/dependencies`, () =>
|
||||
useBaseFetch(`project/${projectId.value}/dependencies`, {}),
|
||||
),
|
||||
useAsyncData(`project/${projectId.value}/version`, () =>
|
||||
useBaseFetch(`project/${projectId.value}/version`),
|
||||
),
|
||||
useAsyncData(`project/${projectId.value}/version/v3`, () =>
|
||||
useBaseFetch(`project/${projectId.value}/version`, { apiVersion: 3 }),
|
||||
),
|
||||
@@ -1541,12 +1513,8 @@ try {
|
||||
|
||||
await updateProjectRoute()
|
||||
|
||||
versions = shallowRef(toRaw(versions))
|
||||
versionsV3 = shallowRef(toRaw(versionsV3))
|
||||
versions.value = (versions.value ?? []).map((v) => ({
|
||||
...v,
|
||||
environment: versionsV3.value?.find((v3) => v3.id === v.id)?.environment,
|
||||
}))
|
||||
versions = shallowRef(toRaw(versionsV3.value?.versions))
|
||||
versionsV3 = shallowRef(toRaw(versionsV3.value))
|
||||
} catch (err) {
|
||||
throw createError({
|
||||
fatal: true,
|
||||
@@ -1584,13 +1552,9 @@ async function resetProject() {
|
||||
}
|
||||
|
||||
async function resetVersions() {
|
||||
await resetVersionsV2()
|
||||
await resetVersionsV3()
|
||||
|
||||
versions.value = (versions.value ?? []).map((v) => ({
|
||||
...v,
|
||||
environment: versionsV3.value?.find((v3) => v3.id === v.id)?.environment,
|
||||
}))
|
||||
versions.value = versionsV3.value?.versions || []
|
||||
}
|
||||
|
||||
function handleError(err, project = false) {
|
||||
@@ -1610,7 +1574,6 @@ handleError(projectV2Error, true)
|
||||
handleError(projectV3Error)
|
||||
handleError(membersError)
|
||||
handleError(dependenciesError)
|
||||
handleError(versionsError)
|
||||
handleError(versionsV3Error)
|
||||
|
||||
if (!project.value) {
|
||||
|
||||
@@ -327,7 +327,7 @@ pub async fn run(
|
||||
.send()
|
||||
.await
|
||||
.and_then(|res| res.error_for_status())
|
||||
.map_err(ApiError::Delphi)?;
|
||||
.map_err(ApiError::delphi)?;
|
||||
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
@@ -411,10 +411,10 @@ async fn issue_type_schema(
|
||||
.send()
|
||||
.await
|
||||
.and_then(|res| res.error_for_status())
|
||||
.map_err(ApiError::Delphi)?
|
||||
.map_err(ApiError::delphi)?
|
||||
.json::<serde_json::Map<String, serde_json::Value>>()
|
||||
.await
|
||||
.map_err(ApiError::Delphi)?,
|
||||
.map_err(ApiError::delphi)?,
|
||||
Instant::now(),
|
||||
))
|
||||
.0,
|
||||
|
||||
@@ -163,8 +163,8 @@ pub enum ApiError {
|
||||
RateLimitError(u128, u32),
|
||||
#[error("Error while interacting with payment processor: {0}")]
|
||||
Stripe(#[from] stripe::StripeError),
|
||||
#[error("Error while interacting with Delphi: {0}")]
|
||||
Delphi(reqwest::Error),
|
||||
#[error("Error while interacting with Delphi: {0:?}")]
|
||||
Delphi(eyre::Error),
|
||||
#[error(transparent)]
|
||||
Mural(#[from] Box<muralpay::ApiError>),
|
||||
#[error("report still has {} issue details with no verdict", details.len())]
|
||||
@@ -174,6 +174,10 @@ pub enum ApiError {
|
||||
}
|
||||
|
||||
impl ApiError {
|
||||
pub fn delphi(err: impl Into<eyre::Error>) -> Self {
|
||||
Self::Delphi(err.into())
|
||||
}
|
||||
|
||||
pub fn as_api_error<'a>(&self) -> crate::models::error::ApiError<'a> {
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
|
||||
@@ -94,10 +94,10 @@ pub async fn version_list(
|
||||
featured: filters.featured,
|
||||
version_type: filters.version_type,
|
||||
limit: filters.limit,
|
||||
offset: filters.offset,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let response = v3::versions::version_list(
|
||||
let response = match v3::versions::version_list(
|
||||
req,
|
||||
info,
|
||||
web::Query(filters),
|
||||
@@ -106,19 +106,20 @@ pub async fn version_list(
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
.or_else(v2_reroute::flatten_404_error)?;
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(ApiError::NotFound) => return Ok(HttpResponse::NotFound().body("")),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// Convert response to V2 format
|
||||
match v2_reroute::extract_ok_json::<Vec<Version>>(response).await {
|
||||
Ok(versions) => {
|
||||
let v2_versions = versions
|
||||
.into_iter()
|
||||
.map(LegacyVersion::from)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(HttpResponse::Ok().json(v2_versions))
|
||||
}
|
||||
Err(response) => Ok(response),
|
||||
}
|
||||
let v2_versions = response
|
||||
.0
|
||||
.versions
|
||||
.into_iter()
|
||||
.map(LegacyVersion::from)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(HttpResponse::Ok().json(v2_versions))
|
||||
}
|
||||
|
||||
// Given a project ID/slug and a version slug
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::models::ids::VersionId;
|
||||
use crate::models::images::ImageContext;
|
||||
use crate::models::pats::Scopes;
|
||||
use crate::models::projects::{
|
||||
Dependency, FileType, VersionStatus, VersionType,
|
||||
Dependency, FileType, Version, VersionStatus, VersionType,
|
||||
};
|
||||
use crate::models::projects::{Loader, skip_nulls};
|
||||
use crate::models::teams::ProjectPermissions;
|
||||
@@ -701,13 +701,13 @@ pub async fn version_edit_helper(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize, Validate, Default, Debug)]
|
||||
pub struct VersionListFilters {
|
||||
pub loaders: Option<String>,
|
||||
pub featured: Option<bool>,
|
||||
pub version_type: Option<VersionType>,
|
||||
pub limit: Option<usize>,
|
||||
pub offset: Option<usize>,
|
||||
pub next_id: Option<VersionId>,
|
||||
/*
|
||||
Loader fields to filter with:
|
||||
"game_versions": ["1.16.5", "1.17"]
|
||||
@@ -717,6 +717,36 @@ pub struct VersionListFilters {
|
||||
pub loader_fields: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct VersionListResponse {
|
||||
pub versions: Vec<Version>,
|
||||
pub available_game_versions: Vec<String>,
|
||||
pub available_loaders: Vec<Loader>,
|
||||
pub latest_versions: LatestVersions,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LatestVersions {
|
||||
pub release: Option<Version>,
|
||||
pub beta: Option<Version>,
|
||||
pub alpha: Option<Version>,
|
||||
}
|
||||
|
||||
fn find_latest_by_type(
|
||||
versions: &[database::models::version_item::VersionQueryResult],
|
||||
version_type: VersionType,
|
||||
after_date: Option<chrono::DateTime<chrono::Utc>>,
|
||||
) -> Option<database::models::version_item::VersionQueryResult> {
|
||||
versions
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
v.inner.version_type.as_str() == version_type.as_str()
|
||||
&& after_date.map_or(true, |d| v.inner.date_published > d)
|
||||
})
|
||||
.max_by_key(|v| v.inner.date_published)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub async fn version_list(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
@@ -724,7 +754,7 @@ pub async fn version_list(
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
) -> Result<web::Json<VersionListResponse>, ApiError> {
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let result =
|
||||
@@ -748,6 +778,37 @@ pub async fn version_list(
|
||||
return Err(ApiError::NotFound);
|
||||
}
|
||||
|
||||
// Fetch all versions first (for computing metadata)
|
||||
let all_versions = database::models::DBVersion::get_many(
|
||||
&project.versions,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Compute available_game_versions and available_loaders from ALL versions
|
||||
let available_game_versions: Vec<String> = all_versions
|
||||
.iter()
|
||||
.flat_map(|v| {
|
||||
v.version_fields
|
||||
.iter()
|
||||
.find(|vf| vf.field_name == "game_versions")
|
||||
.map(|vf| vf.value.as_strings())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.sorted()
|
||||
.dedup()
|
||||
.collect();
|
||||
|
||||
let available_loaders: Vec<Loader> = all_versions
|
||||
.iter()
|
||||
.flat_map(|v| v.loaders.iter().cloned())
|
||||
.sorted()
|
||||
.dedup()
|
||||
.map(Loader)
|
||||
.collect();
|
||||
|
||||
// Parse filter parameters
|
||||
let loader_field_filters = filters.loader_fields.as_ref().map(|x| {
|
||||
serde_json::from_str::<HashMap<String, Vec<serde_json::Value>>>(x)
|
||||
.unwrap_or_default()
|
||||
@@ -755,70 +816,100 @@ pub async fn version_list(
|
||||
let loader_filters = filters.loaders.as_ref().map(|x| {
|
||||
serde_json::from_str::<Vec<String>>(x).unwrap_or_default()
|
||||
});
|
||||
let mut versions = database::models::DBVersion::get_many(
|
||||
&project.versions,
|
||||
&**pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.skip(filters.offset.unwrap_or(0))
|
||||
.take(filters.limit.unwrap_or(usize::MAX))
|
||||
.filter(|x| {
|
||||
let mut bool = true;
|
||||
|
||||
if let Some(version_type) = filters.version_type {
|
||||
bool &= &*x.inner.version_type == version_type.as_str();
|
||||
}
|
||||
if let Some(loaders) = &loader_filters {
|
||||
bool &= x.loaders.iter().any(|y| loaders.contains(y));
|
||||
}
|
||||
if let Some(loader_fields) = &loader_field_filters {
|
||||
for (key, values) in loader_fields {
|
||||
bool &= if let Some(x_vf) =
|
||||
x.version_fields.iter().find(|y| y.field_name == *key)
|
||||
{
|
||||
values.iter().any(|v| x_vf.value.contains_json_value(v))
|
||||
} else {
|
||||
true
|
||||
};
|
||||
// Apply filters to all versions
|
||||
let mut filtered_versions = all_versions
|
||||
.into_iter()
|
||||
.filter(|x| {
|
||||
let mut valid = true;
|
||||
|
||||
if let Some(version_type) = filters.version_type {
|
||||
valid &= &*x.inner.version_type == version_type.as_str();
|
||||
}
|
||||
if let Some(loaders) = &loader_filters {
|
||||
valid &= x.loaders.iter().any(|y| loaders.contains(y));
|
||||
}
|
||||
if let Some(loader_fields) = &loader_field_filters {
|
||||
for (key, values) in loader_fields {
|
||||
valid &= if let Some(x_vf) = x
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|y| y.field_name == *key)
|
||||
{
|
||||
values
|
||||
.iter()
|
||||
.any(|v| x_vf.value.contains_json_value(v))
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
}
|
||||
if let Some(featured) = filters.featured {
|
||||
valid &= featured == x.inner.featured;
|
||||
}
|
||||
}
|
||||
bool
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut response = versions
|
||||
.iter()
|
||||
.filter(|version| {
|
||||
filters
|
||||
.featured
|
||||
.is_none_or(|featured| featured == version.inner.featured)
|
||||
valid
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
versions.sort_by(|a, b| {
|
||||
b.inner.date_published.cmp(&a.inner.date_published)
|
||||
// Sort by (date_published DESC, id DESC)
|
||||
filtered_versions.sort_by(|a, b| {
|
||||
let date_cmp = b.inner.date_published.cmp(&a.inner.date_published);
|
||||
if date_cmp == std::cmp::Ordering::Equal {
|
||||
b.inner.id.0.cmp(&a.inner.id.0)
|
||||
} else {
|
||||
date_cmp
|
||||
}
|
||||
});
|
||||
|
||||
// Clone filtered_versions for later use (auto-featured logic and latest_versions computation)
|
||||
let filtered_versions_for_pagination = filtered_versions.clone();
|
||||
|
||||
// Apply cursor pagination using next_id
|
||||
let paginated_versions: Vec<_> = match filters.next_id {
|
||||
Some(next_id) => {
|
||||
let cursor_index = filtered_versions_for_pagination
|
||||
.iter()
|
||||
.position(|v| v.inner.id == next_id.into());
|
||||
|
||||
match cursor_index {
|
||||
Some(idx) => filtered_versions_for_pagination
|
||||
.into_iter()
|
||||
.skip(idx + 1)
|
||||
.take(filters.limit.unwrap_or(20))
|
||||
.collect(),
|
||||
None => {
|
||||
// Invalid cursor, return first page
|
||||
filtered_versions_for_pagination
|
||||
.into_iter()
|
||||
.take(filters.limit.unwrap_or(20))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => filtered_versions_for_pagination
|
||||
.into_iter()
|
||||
.take(filters.limit.unwrap_or(20))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
// Attempt to populate versions with "auto featured" versions
|
||||
if response.is_empty()
|
||||
&& !versions.is_empty()
|
||||
let mut response = if paginated_versions.is_empty()
|
||||
&& !filtered_versions.is_empty()
|
||||
&& filters.featured.unwrap_or(false)
|
||||
{
|
||||
// TODO: This is a bandaid fix for detecting auto-featured versions.
|
||||
// In the future, not all versions will have 'game_versions' fields, so this will need to be changed.
|
||||
let (loaders, game_versions) = futures::future::try_join(
|
||||
database::models::loader_fields::Loader::list(&**pool, &redis),
|
||||
database::models::legacy_loader_fields::MinecraftGameVersion::list(
|
||||
None,
|
||||
Some(true),
|
||||
&**pool,
|
||||
&redis,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
database::models::loader_fields::Loader::list(&**pool, &redis),
|
||||
database::models::legacy_loader_fields::MinecraftGameVersion::list(
|
||||
None,
|
||||
Some(true),
|
||||
&**pool,
|
||||
&redis,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut joined_filters = Vec::new();
|
||||
for game_version in &game_versions {
|
||||
@@ -827,40 +918,87 @@ pub async fn version_list(
|
||||
}
|
||||
}
|
||||
|
||||
let mut auto_featured = Vec::new();
|
||||
joined_filters.into_iter().for_each(|filter| {
|
||||
if let Some(version) = versions.iter().find(|version| {
|
||||
// TODO: This is the bandaid fix for detecting auto-featured versions.
|
||||
let game_versions = version
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|vf| vf.field_name == "game_versions")
|
||||
.map(|vf| vf.value.clone())
|
||||
.map(|v| v.as_strings())
|
||||
.unwrap_or_default();
|
||||
game_versions.contains(&filter.0.version)
|
||||
&& version.loaders.contains(&filter.1.loader)
|
||||
}) {
|
||||
response.push(version.clone());
|
||||
if let Some(version) =
|
||||
filtered_versions.iter().find(|version| {
|
||||
// TODO: This is the bandaid fix for detecting auto-featured versions.
|
||||
let game_versions = version
|
||||
.version_fields
|
||||
.iter()
|
||||
.find(|vf| vf.field_name == "game_versions")
|
||||
.map(|vf| vf.value.clone())
|
||||
.map(|v| v.as_strings())
|
||||
.unwrap_or_default();
|
||||
game_versions.contains(&filter.0.version)
|
||||
&& version.loaders.contains(&filter.1.loader)
|
||||
})
|
||||
{
|
||||
auto_featured.push(version.clone());
|
||||
}
|
||||
});
|
||||
|
||||
if response.is_empty() {
|
||||
versions
|
||||
.into_iter()
|
||||
.for_each(|version| response.push(version));
|
||||
if auto_featured.is_empty() {
|
||||
filtered_versions.clone()
|
||||
} else {
|
||||
auto_featured
|
||||
}
|
||||
}
|
||||
} else {
|
||||
paginated_versions
|
||||
};
|
||||
|
||||
response.sort_by(|a, b| {
|
||||
b.inner.date_published.cmp(&a.inner.date_published)
|
||||
let date_cmp = b.inner.date_published.cmp(&a.inner.date_published);
|
||||
if date_cmp == std::cmp::Ordering::Equal {
|
||||
b.inner.id.0.cmp(&a.inner.id.0)
|
||||
} else {
|
||||
date_cmp
|
||||
}
|
||||
});
|
||||
response.dedup_by(|a, b| a.inner.id == b.inner.id);
|
||||
|
||||
let response =
|
||||
// Compute latest_versions from filtered versions (before pagination)
|
||||
let filtered_for_latest = filtered_versions.clone();
|
||||
let latest_release = find_latest_by_type(
|
||||
&filtered_for_latest,
|
||||
VersionType::Release,
|
||||
None,
|
||||
);
|
||||
let latest_beta = find_latest_by_type(
|
||||
&filtered_for_latest,
|
||||
VersionType::Beta,
|
||||
latest_release.as_ref().map(|r| r.inner.date_published),
|
||||
);
|
||||
let latest_alpha = find_latest_by_type(
|
||||
&filtered_for_latest,
|
||||
VersionType::Alpha,
|
||||
latest_beta
|
||||
.as_ref()
|
||||
.or(latest_release.as_ref())
|
||||
.map(|v| v.inner.date_published),
|
||||
);
|
||||
|
||||
// Filter visible versions
|
||||
let visible_versions =
|
||||
filter_visible_versions(response, &user_option, &pool, &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(response))
|
||||
// Convert to API models
|
||||
let versions: Vec<Version> =
|
||||
visible_versions.into_iter().map(Version::from).collect();
|
||||
|
||||
let latest_versions = LatestVersions {
|
||||
release: latest_release.map(Version::from),
|
||||
beta: latest_beta.map(Version::from),
|
||||
alpha: latest_alpha.map(Version::from),
|
||||
};
|
||||
|
||||
Ok(web::Json(VersionListResponse {
|
||||
versions,
|
||||
available_game_versions,
|
||||
available_loaders,
|
||||
latest_versions,
|
||||
}))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
POSTGRES_USER: labrinth
|
||||
POSTGRES_PASSWORD: labrinth
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
PGUSER: labrinth
|
||||
healthcheck:
|
||||
test: ['CMD', 'pg_isready', '-U', 'labrinth']
|
||||
interval: 3s
|
||||
|
||||
@@ -13,7 +13,9 @@ const _: () = {
|
||||
use crate::{MuralError, RequestExt};
|
||||
|
||||
impl crate::Client {
|
||||
pub async fn get_all_accounts(&self) -> Result<Vec<Account>, MuralError> {
|
||||
pub async fn get_all_accounts(
|
||||
&self,
|
||||
) -> Result<Vec<Account>, MuralError> {
|
||||
maybe_mock!(self, get_all_accounts());
|
||||
|
||||
self.http_get(|base| format!("{base}/api/accounts"))
|
||||
@@ -21,7 +23,10 @@ const _: () = {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_account(&self, id: AccountId) -> Result<Account, MuralError> {
|
||||
pub async fn get_account(
|
||||
&self,
|
||||
id: AccountId,
|
||||
) -> Result<Account, MuralError> {
|
||||
maybe_mock!(self, get_account(id));
|
||||
|
||||
self.http_get(|base| format!("{base}/api/accounts/{id}"))
|
||||
@@ -43,7 +48,10 @@ const _: () = {
|
||||
|
||||
maybe_mock!(
|
||||
self,
|
||||
create_account(name.as_ref(), description.as_ref().map(AsRef::as_ref))
|
||||
create_account(
|
||||
name.as_ref(),
|
||||
description.as_ref().map(AsRef::as_ref)
|
||||
)
|
||||
);
|
||||
|
||||
let body = Body {
|
||||
@@ -59,7 +67,18 @@ const _: () = {
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct AccountId(pub Uuid);
|
||||
|
||||
@@ -69,7 +69,8 @@ impl fmt::Display for ApiError {
|
||||
|
||||
if !self.params.is_empty() {
|
||||
lines.push("params:".into());
|
||||
lines.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
|
||||
lines
|
||||
.extend(self.params.iter().map(|(k, v)| format!("- {k}: {v}")));
|
||||
}
|
||||
|
||||
lines.push(format!("error name: {}", self.name));
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
use {
|
||||
crate::{
|
||||
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId, CreateCounterparty,
|
||||
CreatePayout, FiatAndRailCode, FiatFeeRequest, FiatPayoutFee, MuralError, Organization,
|
||||
OrganizationId, PayoutMethod, PayoutMethodDetails, PayoutMethodId, PayoutRequest,
|
||||
PayoutRequestId, PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
|
||||
Account, AccountId, BankDetailsResponse, Counterparty, CounterpartyId,
|
||||
CreateCounterparty, CreatePayout, FiatAndRailCode, FiatFeeRequest,
|
||||
FiatPayoutFee, MuralError, Organization, OrganizationId, PayoutMethod,
|
||||
PayoutMethodDetails, PayoutMethodId, PayoutRequest, PayoutRequestId,
|
||||
PayoutStatusFilter, SearchParams, SearchRequest, SearchResponse,
|
||||
TokenFeeRequest, TokenPayoutFee, UpdateCounterparty,
|
||||
transaction::{Transaction, TransactionId},
|
||||
},
|
||||
|
||||
@@ -46,26 +46,40 @@ impl Client {
|
||||
api_url: String::new(),
|
||||
api_key: SecretString::from(String::new()),
|
||||
transfer_api_key: SecretString::from(String::new()),
|
||||
mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(mock)),
|
||||
mock: std::sync::Arc::new(arc_swap::ArcSwapOption::from_pointee(
|
||||
mock,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn http_req(&self, make_req: impl FnOnce() -> RequestBuilder) -> RequestBuilder {
|
||||
fn http_req(
|
||||
&self,
|
||||
make_req: impl FnOnce() -> RequestBuilder,
|
||||
) -> RequestBuilder {
|
||||
make_req()
|
||||
.bearer_auth(self.api_key.expose_secret())
|
||||
.header("accept", "application/json")
|
||||
.header("content-type", "application/json")
|
||||
}
|
||||
|
||||
pub(crate) fn http_get<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder {
|
||||
pub(crate) fn http_get<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.get(make_url(&self.api_url)))
|
||||
}
|
||||
|
||||
pub(crate) fn http_post<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder {
|
||||
pub(crate) fn http_post<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.post(make_url(&self.api_url)))
|
||||
}
|
||||
|
||||
pub(crate) fn http_put<U: IntoUrl>(&self, make_url: impl FnOnce(&str) -> U) -> RequestBuilder {
|
||||
pub(crate) fn http_put<U: IntoUrl>(
|
||||
&self,
|
||||
make_url: impl FnOnce(&str) -> U,
|
||||
) -> RequestBuilder {
|
||||
self.http_req(|| self.http.put(make_url(&self.api_url)))
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ const _: () = {
|
||||
pub async fn search_counterparties(
|
||||
&self,
|
||||
params: Option<SearchParams<CounterpartyId>>,
|
||||
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError> {
|
||||
) -> Result<SearchResponse<CounterpartyId, Counterparty>, MuralError>
|
||||
{
|
||||
maybe_mock!(self, search_counterparties(params));
|
||||
|
||||
self.http_post(|base| format!("{base}/api/counterparties/search"))
|
||||
@@ -30,9 +31,11 @@ const _: () = {
|
||||
) -> Result<Counterparty, MuralError> {
|
||||
maybe_mock!(self, get_counterparty(id));
|
||||
|
||||
self.http_get(|base| format!("{base}/api/counterparties/counterparty/{id}"))
|
||||
.send_mural()
|
||||
.await
|
||||
self.http_get(|base| {
|
||||
format!("{base}/api/counterparties/counterparty/{id}")
|
||||
})
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_counterparty(
|
||||
@@ -70,15 +73,28 @@ const _: () = {
|
||||
|
||||
let body = Body { counterparty };
|
||||
|
||||
self.http_put(|base| format!("{base}/api/counterparties/counterparty/{id}"))
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
self.http_put(|base| {
|
||||
format!("{base}/api/counterparties/counterparty/{id}")
|
||||
})
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct CounterpartyId(pub Uuid);
|
||||
|
||||
@@ -15,7 +15,8 @@ const _: () = {
|
||||
pub async fn search_organizations(
|
||||
&self,
|
||||
req: SearchRequest,
|
||||
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError> {
|
||||
) -> Result<SearchResponse<OrganizationId, Organization>, MuralError>
|
||||
{
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Body {
|
||||
@@ -41,8 +42,9 @@ const _: () = {
|
||||
|
||||
let query = [
|
||||
req.limit.map(|limit| ("limit", limit.to_string())),
|
||||
req.next_id
|
||||
.map(|next_id| ("nextId", next_id.hyphenated().to_string())),
|
||||
req.next_id.map(|next_id| {
|
||||
("nextId", next_id.hyphenated().to_string())
|
||||
}),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -75,7 +77,18 @@ const _: () = {
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct OrganizationId(pub Uuid);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use {
|
||||
crate::{
|
||||
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId, CrcSymbol,
|
||||
DocumentType, EurSymbol, FiatAccountType, MxnSymbol, PenSymbol, UsdSymbol, WalletDetails,
|
||||
ZarSymbol,
|
||||
ArsSymbol, BobSymbol, BrlSymbol, ClpSymbol, CopSymbol, CounterpartyId,
|
||||
CrcSymbol, DocumentType, EurSymbol, FiatAccountType, MxnSymbol,
|
||||
PenSymbol, UsdSymbol, WalletDetails, ZarSymbol,
|
||||
},
|
||||
chrono::{DateTime, Utc},
|
||||
derive_more::{Deref, Display, Error},
|
||||
@@ -21,7 +21,8 @@ const _: () = {
|
||||
&self,
|
||||
counterparty_id: CounterpartyId,
|
||||
params: Option<SearchParams<PayoutMethodId>>,
|
||||
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError> {
|
||||
) -> Result<SearchResponse<PayoutMethodId, PayoutMethod>, MuralError>
|
||||
{
|
||||
maybe_mock!(self, search_payout_methods(counterparty_id, params));
|
||||
|
||||
self.http_post(|base| {
|
||||
@@ -37,7 +38,10 @@ const _: () = {
|
||||
counterparty_id: CounterpartyId,
|
||||
payout_method_id: PayoutMethodId,
|
||||
) -> Result<PayoutMethod, MuralError> {
|
||||
maybe_mock!(self, get_payout_method(counterparty_id, payout_method_id));
|
||||
maybe_mock!(
|
||||
self,
|
||||
get_payout_method(counterparty_id, payout_method_id)
|
||||
);
|
||||
|
||||
self.http_get(|base| {
|
||||
format!(
|
||||
@@ -63,7 +67,11 @@ const _: () = {
|
||||
|
||||
maybe_mock!(
|
||||
self,
|
||||
create_payout_method(counterparty_id, alias.as_ref(), payout_method)
|
||||
create_payout_method(
|
||||
counterparty_id,
|
||||
alias.as_ref(),
|
||||
payout_method
|
||||
)
|
||||
);
|
||||
|
||||
let body = Body {
|
||||
@@ -72,7 +80,9 @@ const _: () = {
|
||||
};
|
||||
|
||||
self.http_post(|base| {
|
||||
format!("{base}/api/counterparties/{counterparty_id}/payout-methods")
|
||||
format!(
|
||||
"{base}/api/counterparties/{counterparty_id}/payout-methods"
|
||||
)
|
||||
})
|
||||
.json(&body)
|
||||
.send_mural()
|
||||
@@ -121,7 +131,18 @@ pub enum PayoutMethodPixAccountType {
|
||||
BankAccount,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct PayoutMethodId(pub Uuid);
|
||||
|
||||
@@ -4,7 +4,10 @@ use {
|
||||
std::borrow::Cow,
|
||||
};
|
||||
|
||||
pub fn serialize<S: serde::Serializer>(v: &CountryCode, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
pub fn serialize<S: serde::Serializer>(
|
||||
v: &CountryCode,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(v.alpha2)
|
||||
}
|
||||
|
||||
@@ -15,6 +18,8 @@ pub fn deserialize<'de, D: serde::Deserializer<'de>>(
|
||||
rust_iso3166::ALPHA2_MAP
|
||||
.get(&country_code)
|
||||
.copied()
|
||||
.ok_or_else(|| D::Error::custom("invalid ISO 3166 alpha-2 country code"))
|
||||
.ok_or_else(|| {
|
||||
D::Error::custom("invalid ISO 3166 alpha-2 country code")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,14 +5,21 @@ use derive_more::{Deref, Display};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount};
|
||||
use crate::{
|
||||
AccountId, Blockchain, FiatAmount, PayoutId, PayoutRequestId, TokenAmount,
|
||||
};
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
const _: () = {
|
||||
use crate::{Account, MuralError, RequestExt, SearchParams, SearchResponse};
|
||||
use crate::{
|
||||
Account, MuralError, RequestExt, SearchParams, SearchResponse,
|
||||
};
|
||||
|
||||
impl crate::Client {
|
||||
pub async fn get_transaction(&self, id: TransactionId) -> Result<Transaction, MuralError> {
|
||||
pub async fn get_transaction(
|
||||
&self,
|
||||
id: TransactionId,
|
||||
) -> Result<Transaction, MuralError> {
|
||||
maybe_mock!(self, get_transaction(id));
|
||||
|
||||
self.http_get(|base| format!("{base}/api/transactions/{id}"))
|
||||
@@ -27,15 +34,28 @@ const _: () = {
|
||||
) -> Result<SearchResponse<AccountId, Account>, MuralError> {
|
||||
maybe_mock!(self, search_transactions(account_id, params));
|
||||
|
||||
self.http_post(|base| format!("{base}/api/transactions/search/account/{account_id}"))
|
||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||
.send_mural()
|
||||
.await
|
||||
self.http_post(|base| {
|
||||
format!("{base}/api/transactions/search/account/{account_id}")
|
||||
})
|
||||
.query(¶ms.map(|p| p.to_query()).unwrap_or_default())
|
||||
.send_mural()
|
||||
.await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, Hash, Deref, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug,
|
||||
Display,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
Deref,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
#[display("{}", _0.hyphenated())]
|
||||
pub struct TransactionId(pub Uuid);
|
||||
|
||||
@@ -5,7 +5,8 @@ macro_rules! display_as_serialize {
|
||||
|
||||
impl fmt::Display for $T {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let value = serde_json::to_value(self).map_err(|_| fmt::Error)?;
|
||||
let value =
|
||||
serde_json::to_value(self).map_err(|_| fmt::Error)?;
|
||||
let value = value.as_str().ok_or(fmt::Error)?;
|
||||
write!(f, "{value}")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user