mirror of
https://github.com/ProvableHQ/leo.git
synced 2024-11-30 23:33:27 +03:00
publish is now part of api module
This commit is contained in:
parent
5adb0ec2d0
commit
87aff4b715
84
leo/api.rs
84
leo/api.rs
@ -16,11 +16,20 @@
|
|||||||
|
|
||||||
use anyhow::{anyhow, Error, Result};
|
use anyhow::{anyhow, Error, Result};
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
blocking::{Client, Response},
|
blocking::{multipart::Form, Client, Response},
|
||||||
Method,
|
Method,
|
||||||
StatusCode,
|
StatusCode,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
||||||
|
/// Format to use.
|
||||||
|
/// Default is JSON, but publish route uses FormData
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum ContentType {
|
||||||
|
JSON,
|
||||||
|
FormData,
|
||||||
|
}
|
||||||
|
|
||||||
/// API Routes and Request bodies.
|
/// API Routes and Request bodies.
|
||||||
/// Structs that implement Route MUST also support Serialize to be usable in Api::run_route(r: Route)
|
/// Structs that implement Route MUST also support Serialize to be usable in Api::run_route(r: Route)
|
||||||
@ -35,6 +44,9 @@ pub trait Route {
|
|||||||
/// The URL path without the first forward slash (e.g. v1/package/fetch)
|
/// The URL path without the first forward slash (e.g. v1/package/fetch)
|
||||||
const PATH: &'static str;
|
const PATH: &'static str;
|
||||||
|
|
||||||
|
/// Content type: JSON or Multipart/FormData. Only usable in POST/PUT queries.
|
||||||
|
const CONTENT_TYPE: ContentType;
|
||||||
|
|
||||||
/// The output type for this route. For example, the login route output is [`String`].
|
/// The output type for this route. For example, the login route output is [`String`].
|
||||||
/// But for other routes may be more complex.
|
/// But for other routes may be more complex.
|
||||||
type Output;
|
type Output;
|
||||||
@ -42,6 +54,11 @@ pub trait Route {
|
|||||||
/// Process the reqwest Response and turn it into an Output.
|
/// Process the reqwest Response and turn it into an Output.
|
||||||
fn process(&self, res: Response) -> Result<Self::Output>;
|
fn process(&self, res: Response) -> Result<Self::Output>;
|
||||||
|
|
||||||
|
/// Represent self as a form data for multipart (ContentType::FormData) requests.
|
||||||
|
fn to_form(&self) -> Option<Form> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Transform specific status codes into correct errors for this route.
|
/// Transform specific status codes into correct errors for this route.
|
||||||
/// For example 404 on package fetch should mean that 'Package is not found'
|
/// For example 404 on package fetch should mean that 'Package is not found'
|
||||||
fn status_to_err(&self, _status: StatusCode) -> Error {
|
fn status_to_err(&self, _status: StatusCode) -> Error {
|
||||||
@ -95,7 +112,16 @@ impl Api {
|
|||||||
|
|
||||||
// add body for POST and PUT requests
|
// add body for POST and PUT requests
|
||||||
if T::METHOD == Method::POST || T::METHOD == Method::PUT {
|
if T::METHOD == Method::POST || T::METHOD == Method::PUT {
|
||||||
res = res.json(&route);
|
res = match T::CONTENT_TYPE {
|
||||||
|
ContentType::JSON => res.json(&route),
|
||||||
|
ContentType::FormData => {
|
||||||
|
let form = route
|
||||||
|
.to_form()
|
||||||
|
.unwrap_or_else(|| unimplemented!("to_form is not implemented for this route"));
|
||||||
|
|
||||||
|
res.multipart(form)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// if Route::Auth is true and token is present - pass it
|
// if Route::Auth is true and token is present - pass it
|
||||||
@ -131,6 +157,7 @@ impl Route for Fetch {
|
|||||||
type Output = Response;
|
type Output = Response;
|
||||||
|
|
||||||
const AUTH: bool = true;
|
const AUTH: bool = true;
|
||||||
|
const CONTENT_TYPE: ContentType = ContentType::JSON;
|
||||||
const METHOD: Method = Method::POST;
|
const METHOD: Method = Method::POST;
|
||||||
const PATH: &'static str = "api/package/fetch";
|
const PATH: &'static str = "api/package/fetch";
|
||||||
|
|
||||||
@ -167,6 +194,7 @@ impl Route for Login {
|
|||||||
type Output = Response;
|
type Output = Response;
|
||||||
|
|
||||||
const AUTH: bool = false;
|
const AUTH: bool = false;
|
||||||
|
const CONTENT_TYPE: ContentType = ContentType::JSON;
|
||||||
const METHOD: Method = Method::POST;
|
const METHOD: Method = Method::POST;
|
||||||
const PATH: &'static str = "api/account/authenticate";
|
const PATH: &'static str = "api/account/authenticate";
|
||||||
|
|
||||||
@ -188,6 +216,57 @@ impl Route for Login {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct Publish {
|
||||||
|
pub name: String,
|
||||||
|
pub remote: String,
|
||||||
|
pub version: String,
|
||||||
|
pub file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PublishResponse {
|
||||||
|
package_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Route for Publish {
|
||||||
|
type Output = String;
|
||||||
|
|
||||||
|
const AUTH: bool = true;
|
||||||
|
const CONTENT_TYPE: ContentType = ContentType::FormData;
|
||||||
|
const METHOD: Method = Method::POST;
|
||||||
|
const PATH: &'static str = "api/package/publish";
|
||||||
|
|
||||||
|
fn to_form(&self) -> Option<Form> {
|
||||||
|
Form::new()
|
||||||
|
.text("name", self.name.clone())
|
||||||
|
.text("remote", self.remote.clone())
|
||||||
|
.text("version", self.version.clone())
|
||||||
|
.file("file", self.file.clone())
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&self, res: Response) -> Result<Self::Output> {
|
||||||
|
let status = res.status();
|
||||||
|
|
||||||
|
if status == StatusCode::OK {
|
||||||
|
let body: PublishResponse = res.json()?;
|
||||||
|
Ok(body.package_id)
|
||||||
|
} else {
|
||||||
|
let res: HashMap<String, String> = res.json()?;
|
||||||
|
Err(match status {
|
||||||
|
StatusCode::BAD_REQUEST => anyhow!("{}", res.get("message").unwrap()),
|
||||||
|
StatusCode::UNAUTHORIZED => anyhow!("You are not logged in. Please use `leo login` to login"),
|
||||||
|
StatusCode::FAILED_DEPENDENCY => anyhow!("This package version is already published"),
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR => {
|
||||||
|
anyhow!("Server error, please contact us at https://github.com/AleoHQ/leo/issues")
|
||||||
|
}
|
||||||
|
_ => anyhow!("Unknown status code"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Handler for 'my_profile' route. Meant to be used to get profile details but
|
/// Handler for 'my_profile' route. Meant to be used to get profile details but
|
||||||
/// in the current application it is used to check if the user is logged in. Any non-200 response
|
/// in the current application it is used to check if the user is logged in. Any non-200 response
|
||||||
/// is treated as Unauthorized.
|
/// is treated as Unauthorized.
|
||||||
@ -204,6 +283,7 @@ impl Route for Profile {
|
|||||||
type Output = Option<String>;
|
type Output = Option<String>;
|
||||||
|
|
||||||
const AUTH: bool = true;
|
const AUTH: bool = true;
|
||||||
|
const CONTENT_TYPE: ContentType = ContentType::JSON;
|
||||||
const METHOD: Method = Method::GET;
|
const METHOD: Method = Method::GET;
|
||||||
const PATH: &'static str = "api/account/my_profile";
|
const PATH: &'static str = "api/account/my_profile";
|
||||||
|
|
||||||
|
@ -15,27 +15,15 @@
|
|||||||
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
|
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use super::build::Build;
|
use super::build::Build;
|
||||||
use crate::{commands::Command, context::Context};
|
use crate::{api::Publish as PublishRoute, commands::Command, context::Context};
|
||||||
use leo_package::{
|
use leo_package::{
|
||||||
outputs::OutputsDirectory,
|
outputs::OutputsDirectory,
|
||||||
root::{ZipFile, AUTHOR_PLACEHOLDER},
|
root::{ZipFile, AUTHOR_PLACEHOLDER},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use reqwest::{
|
|
||||||
blocking::{multipart::Form, Client},
|
|
||||||
header::{HeaderMap, HeaderValue},
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use structopt::StructOpt;
|
use structopt::StructOpt;
|
||||||
|
|
||||||
pub const PUBLISH_URL: &str = "v1/package/publish";
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ResponseJson {
|
|
||||||
package_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish package to Aleo Package Manager
|
/// Publish package to Aleo Package Manager
|
||||||
#[derive(StructOpt, Debug)]
|
#[derive(StructOpt, Debug)]
|
||||||
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
|
||||||
@ -43,7 +31,7 @@ pub struct Publish {}
|
|||||||
|
|
||||||
impl Command for Publish {
|
impl Command for Publish {
|
||||||
type Input = <Build as Command>::Output;
|
type Input = <Build as Command>::Output;
|
||||||
type Output = Option<String>;
|
type Output = String;
|
||||||
|
|
||||||
/// Build program before publishing
|
/// Build program before publishing
|
||||||
fn prelude(&self, context: Context) -> Result<Self::Input> {
|
fn prelude(&self, context: Context) -> Result<Self::Input> {
|
||||||
@ -90,58 +78,19 @@ impl Command for Publish {
|
|||||||
if zip_file.exists_at(&path) {
|
if zip_file.exists_at(&path) {
|
||||||
tracing::debug!("Existing package zip file found. Clearing it to regenerate.");
|
tracing::debug!("Existing package zip file found. Clearing it to regenerate.");
|
||||||
// Remove the existing package zip file
|
// Remove the existing package zip file
|
||||||
ZipFile::new(&package_name).remove(&path)?;
|
zip_file.remove(&path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
zip_file.write(&path)?;
|
zip_file.write(&path)?;
|
||||||
|
|
||||||
let form_data = Form::new()
|
// Make an API request with zip file and package data.
|
||||||
.text("name", package_name.clone())
|
let package_id = context.api.run_route(PublishRoute {
|
||||||
.text("remote", format!("{}/{}", package_remote.author, package_name))
|
name: package_name.clone(),
|
||||||
.text("version", package_version)
|
remote: format!("{}/{}", package_remote.author, package_name),
|
||||||
.file("file", zip_file.get_file_path(&path))?;
|
version: package_version,
|
||||||
|
file: zip_file.get_file_path(&path).into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
// Client for make POST request
|
tracing::info!("Package published successfully with id: {}", &package_id);
|
||||||
let client = Client::new();
|
Ok(package_id)
|
||||||
|
|
||||||
let token = context
|
|
||||||
.api
|
|
||||||
.auth_token()
|
|
||||||
.ok_or_else(|| anyhow!("Login before publishing package: try leo login --help"))?;
|
|
||||||
|
|
||||||
// Headers for request to publish package
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert(
|
|
||||||
"Authorization",
|
|
||||||
HeaderValue::from_str(&format!("{} {}", "Bearer", token)).unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Make a request to publish a package
|
|
||||||
let response = client
|
|
||||||
.post(format!("{}{}", context.api.host(), PUBLISH_URL).as_str())
|
|
||||||
.headers(headers)
|
|
||||||
.multipart(form_data)
|
|
||||||
.send();
|
|
||||||
|
|
||||||
// Get a response result
|
|
||||||
let result: ResponseJson = match response {
|
|
||||||
Ok(json_result) => {
|
|
||||||
let text = json_result.text()?;
|
|
||||||
|
|
||||||
match serde_json::from_str(&text) {
|
|
||||||
Ok(json) => json,
|
|
||||||
Err(_) => {
|
|
||||||
return Err(anyhow!("Package not published: {}", text));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
tracing::warn!("{:?}", error);
|
|
||||||
return Err(anyhow!("Connection unavailable"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!("Package published successfully with id: {}", result.package_id);
|
|
||||||
Ok(Some(result.package_id))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user