publish is now part of api module

This commit is contained in:
damirka 2021-04-27 15:37:23 +03:00
parent 5adb0ec2d0
commit 87aff4b715
2 changed files with 94 additions and 65 deletions

View File

@ -16,11 +16,20 @@
use anyhow::{anyhow, Error, Result};
use reqwest::{
blocking::{Client, Response},
blocking::{multipart::Form, Client, Response},
Method,
StatusCode,
};
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.
/// 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)
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`].
/// But for other routes may be more complex.
type Output;
@ -42,6 +54,11 @@ pub trait Route {
/// Process the reqwest Response and turn it into an 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.
/// For example 404 on package fetch should mean that 'Package is not found'
fn status_to_err(&self, _status: StatusCode) -> Error {
@ -95,7 +112,16 @@ impl Api {
// add body for POST and PUT requests
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
@ -131,6 +157,7 @@ impl Route for Fetch {
type Output = Response;
const AUTH: bool = true;
const CONTENT_TYPE: ContentType = ContentType::JSON;
const METHOD: Method = Method::POST;
const PATH: &'static str = "api/package/fetch";
@ -167,6 +194,7 @@ impl Route for Login {
type Output = Response;
const AUTH: bool = false;
const CONTENT_TYPE: ContentType = ContentType::JSON;
const METHOD: Method = Method::POST;
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
/// in the current application it is used to check if the user is logged in. Any non-200 response
/// is treated as Unauthorized.
@ -204,6 +283,7 @@ impl Route for Profile {
type Output = Option<String>;
const AUTH: bool = true;
const CONTENT_TYPE: ContentType = ContentType::JSON;
const METHOD: Method = Method::GET;
const PATH: &'static str = "api/account/my_profile";

View File

@ -15,27 +15,15 @@
// along with the Leo library. If not, see <https://www.gnu.org/licenses/>.
use super::build::Build;
use crate::{commands::Command, context::Context};
use crate::{api::Publish as PublishRoute, commands::Command, context::Context};
use leo_package::{
outputs::OutputsDirectory,
root::{ZipFile, AUTHOR_PLACEHOLDER},
};
use anyhow::{anyhow, Result};
use reqwest::{
blocking::{multipart::Form, Client},
header::{HeaderMap, HeaderValue},
};
use serde::Deserialize;
use structopt::StructOpt;
pub const PUBLISH_URL: &str = "v1/package/publish";
#[derive(Deserialize)]
struct ResponseJson {
package_id: String,
}
/// Publish package to Aleo Package Manager
#[derive(StructOpt, Debug)]
#[structopt(setting = structopt::clap::AppSettings::ColoredHelp)]
@ -43,7 +31,7 @@ pub struct Publish {}
impl Command for Publish {
type Input = <Build as Command>::Output;
type Output = Option<String>;
type Output = String;
/// Build program before publishing
fn prelude(&self, context: Context) -> Result<Self::Input> {
@ -90,58 +78,19 @@ impl Command for Publish {
if zip_file.exists_at(&path) {
tracing::debug!("Existing package zip file found. Clearing it to regenerate.");
// Remove the existing package zip file
ZipFile::new(&package_name).remove(&path)?;
zip_file.remove(&path)?;
}
zip_file.write(&path)?;
let form_data = Form::new()
.text("name", package_name.clone())
.text("remote", format!("{}/{}", package_remote.author, package_name))
.text("version", package_version)
.file("file", zip_file.get_file_path(&path))?;
// Make an API request with zip file and package data.
let package_id = context.api.run_route(PublishRoute {
name: package_name.clone(),
remote: format!("{}/{}", package_remote.author, package_name),
version: package_version,
file: zip_file.get_file_path(&path).into(),
})?;
// Client for make POST request
let client = Client::new();
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))
tracing::info!("Package published successfully with id: {}", &package_id);
Ok(package_id)
}
}